Skip to content

Commit a7d92f7

Browse files
committed
Added support for V14 metadata decoding.
1 parent 58c63d4 commit a7d92f7

File tree

4 files changed

+90
-24
lines changed

4 files changed

+90
-24
lines changed

async_substrate_interface/async_substrate.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from async_substrate_interface.utils.decoding import (
6666
_determine_if_old_runtime_call,
6767
_bt_decode_to_dict_or_list,
68+
legacy_scale_decode,
6869
)
6970
from async_substrate_interface.utils.storage import StorageKey
7071
from async_substrate_interface.type_registry import _TYPE_REGISTRY
@@ -816,7 +817,7 @@ async def initialize(self):
816817
)
817818

818819
if ss58_prefix_constant:
819-
self.ss58_format = ss58_prefix_constant
820+
self.ss58_format = ss58_prefix_constant.value
820821
self.initialized = True
821822
self._initializing = False
822823

@@ -908,7 +909,7 @@ async def _get_current_block_hash(
908909

909910
async def _load_registry_at_block(
910911
self, block_hash: Optional[str]
911-
) -> tuple[MetadataV15, PortableRegistry]:
912+
) -> tuple[Optional[MetadataV15], Optional[PortableRegistry]]:
912913
# Should be called for any block that fails decoding.
913914
# Possibly the metadata was different.
914915
try:
@@ -922,7 +923,11 @@ async def _load_registry_at_block(
922923
"Client error: Execution failed: Other: Exported method Metadata_metadata_at_version is not found"
923924
in e.args
924925
):
925-
raise MetadataAtVersionNotFound
926+
logger.warning(
927+
"Exported method Metadata_metadata_at_version is not found. This indicates the block is quite old, "
928+
"decoding for this block will use legacy Python decoding."
929+
)
930+
return None, None
926931
else:
927932
raise e
928933
metadata_option_hex_str = metadata_rpc_result["result"]
@@ -964,6 +969,7 @@ async def decode_scale(
964969
return_scale_obj: bool = False,
965970
block_hash: Optional[str] = None,
966971
runtime: Optional[Runtime] = None,
972+
force_legacy: bool = False,
967973
) -> Union[ScaleObj, Any]:
968974
"""
969975
Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string
@@ -979,6 +985,7 @@ async def decode_scale(
979985
block_hash: Hash of the block where the desired runtime is located. Ignored if supplying `runtime`
980986
runtime: Optional Runtime object whose registry to use for decoding. If not specified, runtime will be
981987
loaded based on the block hash specified (or latest block if no block_hash is specified)
988+
force_legacy: Whether to explicitly use legacy Python-only decoding (non bt-decode).
982989
983990
Returns:
984991
Decoded object
@@ -991,10 +998,10 @@ async def decode_scale(
991998
else:
992999
if not runtime:
9931000
runtime = await self.init_runtime(block_hash=block_hash)
994-
runtime_registry = runtime.registry
1001+
if runtime.metadata_v15 is not None or force_legacy is True:
1002+
obj = decode_by_type_string(type_string, runtime.registry, scale_bytes)
9951003
else:
996-
runtime_registry = runtime.registry
997-
obj = decode_by_type_string(type_string, runtime_registry, scale_bytes)
1004+
obj = legacy_scale_decode(type_string, scale_bytes, runtime)
9981005
if return_scale_obj:
9991006
return ScaleObj(obj)
10001007
else:
@@ -1933,7 +1940,12 @@ def convert_event_data(data):
19331940
)
19341941
if storage_obj:
19351942
for item in list(storage_obj):
1936-
events.append(convert_event_data(item))
1943+
try:
1944+
events.append(convert_event_data(item))
1945+
except (
1946+
AttributeError
1947+
): # indicates this was legacy decoded with scalecodec
1948+
events.append(item)
19371949
return events
19381950

19391951
async def get_metadata(self, block_hash=None) -> MetadataV15:
@@ -2909,7 +2921,7 @@ async def _do_runtime_call_old(
29092921
result_vec_u8_bytes = hex_to_bytes(result_data["result"])
29102922
result_bytes = await self.decode_scale(
29112923
"Vec<u8>", result_vec_u8_bytes, runtime=runtime
2912-
)
2924+
) # TODO may need to force_legacy after testing.
29132925

29142926
# Decode result
29152927
# Get correct type
@@ -2945,22 +2957,32 @@ async def runtime_call(
29452957
params = {}
29462958

29472959
try:
2948-
metadata_v15_value = runtime.metadata_v15.value()
2960+
if runtime.metadata_v15 is None:
2961+
_ = self.runtime_config.type_registry["runtime_api"][api]["methods"][
2962+
method
2963+
]
2964+
runtime_api_types = self.runtime_config.type_registry["runtime_api"][
2965+
api
2966+
].get("types", {})
2967+
runtime.runtime_config.update_type_registry_types(runtime_api_types)
2968+
return await self._do_runtime_call_old(
2969+
api, method, params, block_hash, runtime=runtime
2970+
)
29492971

2950-
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
2951-
api_entry = apis[api]
2952-
methods = {entry["name"]: entry for entry in api_entry["methods"]}
2953-
runtime_call_def = methods[method]
2972+
else:
2973+
metadata_v15_value = runtime.metadata_v15.value()
2974+
2975+
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
2976+
api_entry = apis[api]
2977+
methods = {entry["name"]: entry for entry in api_entry["methods"]}
2978+
runtime_call_def = methods[method]
2979+
if _determine_if_old_runtime_call(runtime_call_def, metadata_v15_value):
2980+
return await self._do_runtime_call_old(
2981+
api, method, params, block_hash, runtime=runtime
2982+
)
29542983
except KeyError:
29552984
raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry")
29562985

2957-
if _determine_if_old_runtime_call(runtime_call_def, metadata_v15_value):
2958-
result = await self._do_runtime_call_old(
2959-
api, method, params, block_hash, runtime=runtime
2960-
)
2961-
2962-
return result
2963-
29642986
if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]):
29652987
raise ValueError(
29662988
f"Number of parameter provided ({len(params)}) does not "

async_substrate_interface/sync_substrate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ def initialize(self):
560560
"System", "SS58Prefix", block_hash=self.last_block_hash
561561
)
562562
if ss58_prefix_constant:
563-
self.ss58_format = ss58_prefix_constant
563+
self.ss58_format = ss58_prefix_constant.value
564564
self.initialized = True
565565

566566
def __exit__(self, exc_type, exc_val, exc_tb):

async_substrate_interface/utils/decoding.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Union, TYPE_CHECKING
22

33
from bt_decode import AxonInfo, PrometheusInfo, decode_list
4+
from scalecodec import ScaleBytes
45

56
from async_substrate_interface.utils import hex_to_bytes
67
from async_substrate_interface.types import ScaleObj
@@ -55,10 +56,16 @@ def _bt_decode_to_dict_or_list(obj) -> Union[dict, list[dict]]:
5556
def _decode_scale_list_with_runtime(
5657
type_strings: list[str],
5758
scale_bytes_list: list[bytes],
58-
runtime_registry,
59+
runtime: "Runtime",
5960
return_scale_obj: bool = False,
6061
):
61-
obj = decode_list(type_strings, runtime_registry, scale_bytes_list)
62+
if runtime.metadata_v15 is not None:
63+
obj = decode_list(type_strings, runtime.registry, scale_bytes_list)
64+
else:
65+
obj = [
66+
legacy_scale_decode(x, y, runtime)
67+
for (x, y) in zip(type_strings, scale_bytes_list)
68+
]
6269
if return_scale_obj:
6370
return [ScaleObj(x) for x in obj]
6471
else:
@@ -109,7 +116,7 @@ def concat_hash_len(key_hasher: str) -> int:
109116
all_decoded = _decode_scale_list_with_runtime(
110117
pre_decoded_key_types + pre_decoded_value_types,
111118
pre_decoded_keys + pre_decoded_values,
112-
runtime.registry,
119+
runtime,
113120
)
114121
middl_index = len(all_decoded) // 2
115122
decoded_keys = all_decoded[:middl_index]
@@ -132,3 +139,18 @@ def concat_hash_len(key_hasher: str) -> int:
132139
item_value = dv
133140
result.append([item_key, item_value])
134141
return result
142+
143+
144+
def legacy_scale_decode(
145+
type_string: str, scale_bytes: Union[str, ScaleBytes], runtime: "Runtime"
146+
):
147+
if isinstance(scale_bytes, (str, bytes)):
148+
scale_bytes = ScaleBytes(scale_bytes)
149+
150+
obj = runtime.runtime_config.create_scale_object(
151+
type_string=type_string, data=scale_bytes, metadata=runtime.metadata
152+
)
153+
154+
obj.decode(check_remaining=runtime.config.get("strict_scale_decode"))
155+
156+
return obj.value

tests/unit_tests/asyncio_/test_substrate_interface.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from async_substrate_interface.types import ScaleObj
99

1010

11+
ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443"
12+
13+
1114
@pytest.mark.asyncio
1215
async def test_invalid_url_raises_exception():
1316
"""Test that invalid URI raises an InvalidURI exception."""
@@ -113,3 +116,22 @@ async def test_websocket_shutdown_timer():
113116
await substrate.get_chain_head()
114117
await asyncio.sleep(6) # same sleep time as before
115118
assert substrate.ws._initialized is True # connection should still be open
119+
120+
121+
@pytest.mark.asyncio
122+
async def test_legacy_decoding():
123+
pre_metadata_v15_block = 3_014_300 # several blocks before metadata v15 was added
124+
125+
async with AsyncSubstrateInterface(ARCHIVE_ENTRYPOINT) as substrate:
126+
block_hash = await substrate.get_block_hash(pre_metadata_v15_block)
127+
events = await substrate.get_events(block_hash)
128+
assert isinstance(events, list)
129+
130+
query_map_result = await substrate.query_map(
131+
module="SubtensorModule",
132+
storage_function="NetworksAdded",
133+
block_hash=block_hash,
134+
)
135+
async for key, value in query_map_result:
136+
assert isinstance(key, int)
137+
assert isinstance(value, ScaleObj)

0 commit comments

Comments
 (0)