From b32fa3b5bbcc840b268b4e65bdc59e5cfdb60ddf Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 15 Feb 2025 20:38:14 -0600 Subject: [PATCH 01/15] fix readme --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa8f1cd..6331d7d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ -This is a modernised version of the py-substrate-interface library, with the ability to use it asynchronously (as well as synchronously). It aims to be almost fully API-compatible with the original library. +# Async Substrate Interface -In addition to it's async nature, it is additionally improved with using bt-decode rather than py-scale-codec for significantly faster SCALE decoding. +This project provides an asynchronous interface for interacting with [Substrate](https://substrate.io/)-based blockchains. It is based on the [py-substrate-interface](https://github.com/polkascan/py-substrate-interface) project. + +## Features + +- Asynchronous API calls +- Support for multiple Substrate-based networks +- Easy integration with existing projects + +## Installation + +To install the package, use the following command: + +```bash +pip install async-substrate-interface +``` + +## Usage + +Here is a basic example of how to use the async-substrate-interface: + +```python +import asyncio +from async_substrate_interface import SubstrateInterface + +async def main(): + substrate = SubstrateInterface( + url="wss://rpc.polkadot.io" + ) + + result = await substrate.query( + module='System', + storage_function='Account', + params=['5FHneW46xGXgs5mUiveU4sbTyGBzmto4oT9v5TFn5u4tZ7sY'] + ) + + print(result) + +asyncio.run(main()) +``` + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Contact + +For any questions or inquiries, please join the Bittensor Development Discord server: [Church of Rao](https://discord.gg/gavmT4R8sB). From 297bffeba801f0320b75efde9678a40477f3d0b2 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 15 Feb 2025 20:43:02 -0600 Subject: [PATCH 02/15] fix readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 6331d7d..d009383 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Async Substrate Interface - This project provides an asynchronous interface for interacting with [Substrate](https://substrate.io/)-based blockchains. It is based on the [py-substrate-interface](https://github.com/polkascan/py-substrate-interface) project. ## Features - Asynchronous API calls -- Support for multiple Substrate-based networks -- Easy integration with existing projects +- Uses [bt-decode](https://github.com/opentensor/bt-decode) instead of [py-scale-codec](https://github.com/polkascan/py-scale-codec) for faster [SCALE](https://polkascan.github.io/py-scale-codec/) decoding. ## Installation From 9cc00235c3e628e55a3c98b0d08b495d3abda334 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 17 Feb 2025 17:26:59 +0200 Subject: [PATCH 03/15] Dynamically pulls the info for Vec from the metadata --- async_substrate_interface/async_substrate.py | 10 ++++++++++ async_substrate_interface/sync_substrate.py | 10 ++++++++++ async_substrate_interface/types.py | 10 +++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 3cb2189..523b333 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -717,6 +717,7 @@ def __init__( self.metadata_version_hex = "0x0f000000" # v15 self.reload_type_registry() self._initializing = False + self.registry_type_map = {} async def __aenter__(self): await self.initialize() @@ -806,6 +807,15 @@ async def load_registry(self): metadata_option_bytes ) self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15) + registry_type_map = {} + for i in json.loads(self.registry.registry)["types"]: + for variants in ( + i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) + ): + for field in variants.get("fields", [{}]): + if field.get("type") and field.get("typeName"): + field["typeName"] = registry_type_map[field["type"]] + self.registry_type_map = registry_type_map async def _load_registry_at_block(self, block_hash: str) -> MetadataV15: # Should be called for any block that fails decoding. diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index f0a9ef2..d8bf212 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -516,6 +516,7 @@ def __init__( self.metadata_version_hex = "0x0f000000" # v15 self.reload_type_registry() self.ws = self.connect(init=True) + self.registry_type_map = {} if not _mock: self.initialize() @@ -610,6 +611,15 @@ def load_registry(self): metadata_option_bytes ) self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15) + registry_type_map = {} + for i in json.loads(self.registry.registry)["types"]: + for variants in ( + i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) + ): + for field in variants.get("fields", [{}]): + if field.get("type") and field.get("typeName"): + field["typeName"] = registry_type_map[field["type"]] + self.registry_type_map = registry_type_map def _load_registry_at_block(self, block_hash: str) -> MetadataV15: # Should be called for any block that fails decoding. diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index daaaafc..f8b8661 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -345,6 +345,7 @@ class SubstrateMixin(ABC): type_registry: Optional[dict] ss58_format: Optional[int] ws_max_size = 2**32 + registry_type_map: dict[str, int] @property def chain(self): @@ -722,12 +723,19 @@ def _encode_scale(self, type_string, value: Any) -> bytes: if value is None: result = b"\x00" else: + try: + vec_acct_id = ( + f"scale_info::{self.registry_type_map['Vec']}" + ) + except KeyError: + vec_acct_id = "scale_info::152" + if type_string == "scale_info::0": # Is an AccountId # encode string into AccountId ## AccountId is a composite type with one, unnamed field return bytes.fromhex(ss58_decode(value, SS58_FORMAT)) - elif type_string == "scale_info::151": # Vec + elif type_string == vec_acct_id: # Vec if not isinstance(value, (list, tuple)): value = [value] From 924f8c848dc9002eb1be114accba9b26427aada0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 13:12:48 +0200 Subject: [PATCH 04/15] Accidentally had it backwards. --- async_substrate_interface/async_substrate.py | 2 +- async_substrate_interface/sync_substrate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 523b333..d93921a 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -814,7 +814,7 @@ async def load_registry(self): ): for field in variants.get("fields", [{}]): if field.get("type") and field.get("typeName"): - field["typeName"] = registry_type_map[field["type"]] + registry_type_map[field["typeName"]] = field["type"] self.registry_type_map = registry_type_map async def _load_registry_at_block(self, block_hash: str) -> MetadataV15: diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index d8bf212..587ad58 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -618,7 +618,7 @@ def load_registry(self): ): for field in variants.get("fields", [{}]): if field.get("type") and field.get("typeName"): - field["typeName"] = registry_type_map[field["type"]] + registry_type_map[field["typeName"]] = field["type"] self.registry_type_map = registry_type_map def _load_registry_at_block(self, block_hash: str) -> MetadataV15: @@ -1692,6 +1692,7 @@ def _make_rpc_request( item_id = 0 for payload in payloads: item_id += 1 + print({**payload["payload"], **{"id": item_id}}) ws.send(json.dumps({**payload["payload"], **{"id": item_id}})) request_manager.add_request(item_id, payload["id"]) From 39d8014bef6d5dc60872239f83d54837e9644c3b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 13:13:33 +0200 Subject: [PATCH 05/15] Remove print. --- async_substrate_interface/sync_substrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 587ad58..50e5c4c 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -1692,7 +1692,6 @@ def _make_rpc_request( item_id = 0 for payload in payloads: item_id += 1 - print({**payload["payload"], **{"id": item_id}}) ws.send(json.dumps({**payload["payload"], **{"id": item_id}})) request_manager.add_request(item_id, payload["id"]) From a0d566e1dec0055af1102002beac1fd652d41b22 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 13:18:19 +0200 Subject: [PATCH 06/15] Consolidate code. --- async_substrate_interface/async_substrate.py | 10 +--------- async_substrate_interface/sync_substrate.py | 10 +--------- async_substrate_interface/types.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index d93921a..4d7dce8 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -807,15 +807,7 @@ async def load_registry(self): metadata_option_bytes ) self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15) - registry_type_map = {} - for i in json.loads(self.registry.registry)["types"]: - for variants in ( - i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) - ): - for field in variants.get("fields", [{}]): - if field.get("type") and field.get("typeName"): - registry_type_map[field["typeName"]] = field["type"] - self.registry_type_map = registry_type_map + self._load_registry_type_map() async def _load_registry_at_block(self, block_hash: str) -> MetadataV15: # Should be called for any block that fails decoding. diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 50e5c4c..35f0df6 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -611,15 +611,7 @@ def load_registry(self): metadata_option_bytes ) self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15) - registry_type_map = {} - for i in json.loads(self.registry.registry)["types"]: - for variants in ( - i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) - ): - for field in variants.get("fields", [{}]): - if field.get("type") and field.get("typeName"): - registry_type_map[field["typeName"]] = field["type"] - self.registry_type_map = registry_type_map + self._load_registry_type_map() def _load_registry_at_block(self, block_hash: str) -> MetadataV15: # Should be called for any block that fails decoding. diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index f8b8661..c09bae8 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -13,6 +13,8 @@ from scalecodec.type_registry import load_type_registry_preset from scalecodec.types import GenericCall, ScaleType +from .utils import json + class RuntimeCache: blocks: dict[int, "Runtime"] @@ -346,6 +348,7 @@ class SubstrateMixin(ABC): ss58_format: Optional[int] ws_max_size = 2**32 registry_type_map: dict[str, int] + metadata_v15 = None @property def chain(self): @@ -601,6 +604,17 @@ def serialize_module_error(module, error, spec_version) -> dict: "spec_version": spec_version, } + def _load_registry_type_map(self): + registry_type_map = {} + for i in json.loads(self.registry.registry)["types"]: + for variants in ( + i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) + ): + for field in variants.get("fields", [{}]): + if field.get("type") and field.get("typeName"): + registry_type_map[field["typeName"]] = field["type"] + self.registry_type_map = registry_type_map + def reload_type_registry( self, use_remote_preset: bool = True, auto_discover: bool = True ): From a4d9d6d15f0eb2e34406a81129d2b10caf6af430 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 13:38:50 +0200 Subject: [PATCH 07/15] Generate UIDs for websockets in both sync and async versions. --- async_substrate_interface/async_substrate.py | 15 +++++---------- async_substrate_interface/sync_substrate.py | 6 +++--- async_substrate_interface/utils/__init__.py | 6 ++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 9cad7e0..579201b 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -48,7 +48,7 @@ SubstrateMixin, Preprocessed, ) -from async_substrate_interface.utils import hex_to_bytes, json +from async_substrate_interface.utils import hex_to_bytes, json, generate_unique_id from async_substrate_interface.utils.decoding import ( _determine_if_old_runtime_call, _bt_decode_to_dict_or_list, @@ -507,7 +507,6 @@ def __init__( # TODO reconnection logic self.ws_url = ws_url self.ws: Optional["ClientConnection"] = None - self.id = 0 self.max_subscriptions = max_subscriptions self.max_connections = max_connections self.shutdown_timer = shutdown_timer @@ -543,8 +542,6 @@ async def connect(self, force=False): connect(self.ws_url, **self._options), timeout=10 ) self._receiving_task = asyncio.create_task(self._start_receiving()) - if force: - self.id = 100 async def __aexit__(self, exc_type, exc_val, exc_tb): async with self._lock: # TODO is this actually what I want to happen? @@ -556,7 +553,6 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): except asyncio.CancelledError: pass if self._in_use == 0 and self.ws is not None: - self.id = 0 self._open_subscriptions = 0 self._exit_task = asyncio.create_task(self._exit_with_timer()) @@ -582,7 +578,6 @@ async def shutdown(self): self.ws = None self._initialized = False self._receiving_task = None - self.id = 0 async def _recv(self) -> None: try: @@ -625,8 +620,7 @@ async def send(self, payload: dict) -> int: id: the internal ID of the request (incremented int) """ # async with self._lock: - original_id = self.id - self.id += 1 + original_id = generate_unique_id(json.dumps(payload)) # self._open_subscriptions += 1 try: await self.ws.send(json.dumps({**payload, **{"id": original_id}})) @@ -735,8 +729,9 @@ async def initialize(self): chain = await self.rpc_request("system_chain", []) self._chain = chain.get("result") init_load = await asyncio.gather( - self.load_registry(), self._first_initialize_runtime(), - return_exceptions=True + self.load_registry(), + self._first_initialize_runtime(), + return_exceptions=True, ) for potential_exception in init_load: if isinstance(potential_exception, Exception): diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 8bb5211..4fa79a5 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -30,7 +30,7 @@ Preprocessed, ScaleObj, ) -from async_substrate_interface.utils import hex_to_bytes, json +from async_substrate_interface.utils import hex_to_bytes, json, generate_unique_id from async_substrate_interface.utils.decoding import ( _determine_if_old_runtime_call, _bt_decode_to_dict_or_list, @@ -1681,9 +1681,9 @@ def _make_rpc_request( subscription_added = False ws = self.connect(init=False if attempt == 1 else True) - item_id = 0 for payload in payloads: - item_id += 1 + payload_str = json.dumps(payload["payload"]) + item_id = generate_unique_id(payload_str) ws.send(json.dumps({**payload["payload"], **{"id": item_id}})) request_manager.add_request(item_id, payload["id"]) diff --git a/async_substrate_interface/utils/__init__.py b/async_substrate_interface/utils/__init__.py index 40e26c5..29b3ced 100644 --- a/async_substrate_interface/utils/__init__.py +++ b/async_substrate_interface/utils/__init__.py @@ -1,4 +1,10 @@ import importlib +import hashlib + + +def generate_unique_id(item: str, length=10): + hashed_value = hashlib.sha256(item.encode()).hexdigest() + return hashed_value[:length] def hex_to_bytes(hex_str: str) -> bytes: From eee4ac48925231f9b7503d8e90f2512b7d11d902 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 15:13:45 +0200 Subject: [PATCH 08/15] More robust type map pulling from metadata --- async_substrate_interface/async_substrate.py | 1 + async_substrate_interface/sync_substrate.py | 1 + async_substrate_interface/types.py | 68 ++++++++++++++++++-- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 4d7dce8..a99032f 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -718,6 +718,7 @@ def __init__( self.reload_type_registry() self._initializing = False self.registry_type_map = {} + self.type_id_to_name = {} async def __aenter__(self): await self.initialize() diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 35f0df6..b75ed2d 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -517,6 +517,7 @@ def __init__( self.reload_type_registry() self.ws = self.connect(init=True) self.registry_type_map = {} + self.type_id_to_name = {} if not _mock: self.initialize() diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index c09bae8..5cf5a89 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -348,6 +348,7 @@ class SubstrateMixin(ABC): ss58_format: Optional[int] ws_max_size = 2**32 registry_type_map: dict[str, int] + type_id_to_name: dict[int, str] metadata_v15 = None @property @@ -606,14 +607,67 @@ def serialize_module_error(module, error, spec_version) -> dict: def _load_registry_type_map(self): registry_type_map = {} - for i in json.loads(self.registry.registry)["types"]: - for variants in ( - i.get("type").get("def", {}).get("variant", {}).get("variants", [{}]) - ): - for field in variants.get("fields", [{}]): - if field.get("type") and field.get("typeName"): - registry_type_map[field["typeName"]] = field["type"] + type_id_to_name = {} + types = json.loads(self.registry.registry)["types"] + for type_entry in types: + type_type = type_entry["type"] + type_id = type_entry["id"] + type_def = type_type["def"] + type_path = type_type.get("path") + if type_entry.get("params") or type_def.get("variant"): + continue # has generics or is Enum + if type_path: + type_name = type_path[-1] + registry_type_map[type_name] = type_id + type_id_to_name[type_id] = type_name + else: + # probably primitive + if type_def.get("primitive"): + type_name = type_def["primitive"] + registry_type_map[type_name] = type_id + type_id_to_name[type_id] = type_name + for type_entry in types: + type_type = type_entry["type"] + type_id = type_entry["id"] + type_def = type_type["def"] + if type_def.get("sequence"): + sequence_type_id = type_def["sequence"]["type"] + inner_type = type_id_to_name.get(sequence_type_id) + if inner_type: + type_name = f"Vec<{inner_type}>" + type_id_to_name[type_id] = type_name + registry_type_map[type_name] = type_id + elif type_def.get("array"): + array_type_id = type_def["array"]["type"] + inner_type = type_id_to_name.get(array_type_id) + maybe_len = type_def["array"].get("len") + if inner_type: + if maybe_len: + type_name = f"[{inner_type}; {maybe_len}]" + else: + type_name = f"[{inner_type}]" + type_id_to_name[type_id] = type_name + registry_type_map[type_name] = type_id + elif type_def.get("compact"): + compact_type_id = type_def["compact"]["type"] + inner_type = type_id_to_name.get(compact_type_id) + if inner_type: + type_name = f"Compact<{inner_type}>" + type_id_to_name[type_id] = type_name + registry_type_map[type_name] = type_id + elif type_def.get("tuple"): + tuple_type_ids = type_def["tuple"] + type_names = [] + for inner_type_id in tuple_type_ids: + inner_type = type_id_to_name.get(inner_type_id) + if inner_type: + type_names.append(inner_type) + type_name = ", ".join(type_names) + type_name = f"({type_name})" + type_id_to_name[type_id] = type_name + registry_type_map[type_name] = type_id self.registry_type_map = registry_type_map + self.type_id_to_name = type_id_to_name def reload_type_registry( self, use_remote_preset: bool = True, auto_discover: bool = True From f40c8ee304df6d4ef862238e0cfd8b530cd345aa Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 16:35:31 +0200 Subject: [PATCH 09/15] Closes the connection on the object being garbage-collected. --- async_substrate_interface/sync_substrate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 8bb5211..f6cb4e1 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -525,6 +525,9 @@ def __enter__(self): self.initialize() return self + def __del__(self): + self.close() + def initialize(self): """ Initialize the connection to the chain. From 2b0b4237b696e08d96b6378b481321a3fc469570 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 18 Feb 2025 09:45:22 -0600 Subject: [PATCH 10/15] fix feedback --- README.md | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d009383..cafeb79 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # Async Substrate Interface This project provides an asynchronous interface for interacting with [Substrate](https://substrate.io/)-based blockchains. It is based on the [py-substrate-interface](https://github.com/polkascan/py-substrate-interface) project. -## Features - -- Asynchronous API calls -- Uses [bt-decode](https://github.com/opentensor/bt-decode) instead of [py-scale-codec](https://github.com/polkascan/py-scale-codec) for faster [SCALE](https://polkascan.github.io/py-scale-codec/) decoding. +Additionally, this project uses [bt-decode](https://github.com/opentensor/bt-decode) instead of [py-scale-codec](https://github.com/polkascan/py-scale-codec) for faster [SCALE](https://docs.substrate.io/reference/scale-codec/) decoding. ## Installation @@ -16,31 +13,50 @@ pip install async-substrate-interface ## Usage -Here is a basic example of how to use the async-substrate-interface: +Here are examples of how to use the sync and async inferfaces: ```python -import asyncio from async_substrate_interface import SubstrateInterface -async def main(): +def main(): substrate = SubstrateInterface( url="wss://rpc.polkadot.io" ) - result = await substrate.query( + result = substrate.query( module='System', storage_function='Account', - params=['5FHneW46xGXgs5mUiveU4sbTyGBzmto4oT9v5TFn5u4tZ7sY'] + params=['5CZs3T15Ky4jch1sUpSFwkUbYEnsCfe1WCY51fH3SPV6NFnf'] ) print(result) +main() +``` + +```python +import asyncio +from async_substrate_interface import AsyncSubstrateInterface + +async def main(): + substrate = AsyncSubstrateInterface( + url="wss://rpc.polkadot.io" + ) + async with substrate: + result = await substrate.query( + module='System', + storage_function='Account', + params=['5CZs3T15Ky4jch1sUpSFwkUbYEnsCfe1WCY51fH3SPV6NFnf'] + ) + + print(result) + asyncio.run(main()) ``` ## Contributing -Contributions are welcome! Please open an issue or submit a pull request. +Contributions are welcome! Please open an issue or submit a pull request to the `staging` branch. ## License @@ -48,4 +64,4 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file ## Contact -For any questions or inquiries, please join the Bittensor Development Discord server: [Church of Rao](https://discord.gg/gavmT4R8sB). +For any questions or inquiries, please join the Bittensor Development Discord server: [Church of Rao](https://discord.gg/XC7ucQmq2Q). From 29e5efcee7e4b583d6c5a38635706ed96bc598ed Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 17:50:13 +0200 Subject: [PATCH 11/15] Name change due to new registry map method. --- async_substrate_interface/async_substrate.py | 5 +++-- async_substrate_interface/types.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index fd4d5ed..5ea6031 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -737,8 +737,9 @@ async def initialize(self): chain = await self.rpc_request("system_chain", []) self._chain = chain.get("result") init_load = await asyncio.gather( - self.load_registry(), self._first_initialize_runtime(), - return_exceptions=True + self.load_registry(), + self._first_initialize_runtime(), + return_exceptions=True, ) for potential_exception in init_load: if isinstance(potential_exception, Exception): diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index 8500a11..6a70db8 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -797,7 +797,7 @@ def _encode_scale(self, type_string, value: Any) -> bytes: else: try: vec_acct_id = ( - f"scale_info::{self.registry_type_map['Vec']}" + f"scale_info::{self.registry_type_map['Vec']}" ) except KeyError: vec_acct_id = "scale_info::152" From 15927d6e37740e811ad103f37bb07795f98f088c Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 18 Feb 2025 10:00:37 -0600 Subject: [PATCH 12/15] add sync context manager --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cafeb79..6b78d0d 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,14 @@ def main(): substrate = SubstrateInterface( url="wss://rpc.polkadot.io" ) + with substrate: + result = substrate.query( + module='System', + storage_function='Account', + params=['5CZs3T15Ky4jch1sUpSFwkUbYEnsCfe1WCY51fH3SPV6NFnf'] + ) - result = substrate.query( - module='System', - storage_function='Account', - params=['5CZs3T15Ky4jch1sUpSFwkUbYEnsCfe1WCY51fH3SPV6NFnf'] - ) - - print(result) + print(result) main() ``` From 68a29953033ef0c1d05c2a65d8c0026c4d375fb8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 18 Feb 2025 22:58:34 +0200 Subject: [PATCH 13/15] bt-decode handles options now --- async_substrate_interface/async_substrate.py | 13 +++++-------- async_substrate_interface/sync_substrate.py | 12 ++++-------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 0037bc6..7713e00 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -892,15 +892,12 @@ async def decode_scale( Returns: Decoded object """ - if scale_bytes == b"\x00": - obj = None + if type_string == "scale_info::0": # Is an AccountId + # Decode AccountId bytes to SS58 address + return ss58_encode(scale_bytes, SS58_FORMAT) else: - if type_string == "scale_info::0": # Is an AccountId - # Decode AccountId bytes to SS58 address - return ss58_encode(scale_bytes, SS58_FORMAT) - else: - await self._wait_for_registry(_attempt, _retries) - obj = decode_by_type_string(type_string, self.registry, scale_bytes) + await self._wait_for_registry(_attempt, _retries) + obj = decode_by_type_string(type_string, self.registry, scale_bytes) if return_scale_obj: return ScaleObj(obj) else: diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index f2756bc..d489ff8 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -652,15 +652,11 @@ def decode_scale( Returns: Decoded object """ - - if scale_bytes == b"\x00": - obj = None + if type_string == "scale_info::0": # Is an AccountId + # Decode AccountId bytes to SS58 address + return ss58_encode(scale_bytes, SS58_FORMAT) else: - if type_string == "scale_info::0": # Is an AccountId - # Decode AccountId bytes to SS58 address - return ss58_encode(scale_bytes, SS58_FORMAT) - else: - obj = decode_by_type_string(type_string, self.registry, scale_bytes) + obj = decode_by_type_string(type_string, self.registry, scale_bytes) if return_scale_obj: return ScaleObj(obj) else: From efe558872b22ace7f5a2e14fdd43047047f8f62b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Feb 2025 16:27:09 -0800 Subject: [PATCH 14/15] Handles none change data --- async_substrate_interface/async_substrate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 7713e00..be112be 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -892,6 +892,8 @@ async def decode_scale( Returns: Decoded object """ + if scale_bytes == b"": + return None if type_string == "scale_info::0": # Is an AccountId # Decode AccountId bytes to SS58 address return ss58_encode(scale_bytes, SS58_FORMAT) @@ -2230,7 +2232,7 @@ async def query_multi( # Decode result for specified storage_key storage_key = storage_key_map[change_storage_key] if change_data is None: - change_data = b"\x00" + change_data = b"" else: change_data = bytes.fromhex(change_data[2:]) result.append( From c0303147171e6a981b56e23779682338a4bbed5c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Feb 2025 16:37:16 -0800 Subject: [PATCH 15/15] Bumps version and changelog --- CHANGELOG.md | 18 ++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 166e9bf..0d841ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.0.2 /2025-02-19 + +## What's Changed +* Closes the connection on the object being garbage-collected by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/51 +* Generate UIDs for websockets by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/50 +* Dynamically pulls the info for Vec from the metadata by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/47 +* Fix readme by @igorsyl in https://github.com/opentensor/async-substrate-interface/pull/46 +* Handle options with bt-decode by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/52 +* Backmerge main to staging 101 by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/53 +* Handles None change_data by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/54 + +## New Contributors +* @igorsyl made their first contribution in https://github.com/opentensor/async-substrate-interface/pull/46 + +**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.0.1...v1.0.2 + +## 1.0.1 /2025-02-17 + ## What's Changed * Updates type for vec acc id by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/45 * Backmerge main staging 101 by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/48 diff --git a/pyproject.toml b/pyproject.toml index a70e722..b2547f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "async-substrate-interface" -version = "1.0.1" +version = "1.0.2" description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface" readme = "README.md" license = { file = "LICENSE" }