From 54f104e01129a49bd379dc12fefeb131d8faf98a Mon Sep 17 00:00:00 2001 From: Thierry Moisan Date: Mon, 13 Oct 2025 21:31:24 -0400 Subject: [PATCH 01/12] Support python 3.14 --- .github/workflows/e2e-tests.yml | 2 +- .github/workflows/unit-and-integration-test.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4bd819b..a28107d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -124,7 +124,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check-out repository uses: actions/checkout@v4 diff --git a/.github/workflows/unit-and-integration-test.yml b/.github/workflows/unit-and-integration-test.yml index 7bc70ae..880dae0 100644 --- a/.github/workflows/unit-and-integration-test.yml +++ b/.github/workflows/unit-and-integration-test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout repository diff --git a/pyproject.toml b/pyproject.toml index 2fd3b5b..922434a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "aiosqlite>=0.21.0,<1.0.0" ] -requires-python = ">=3.9,<3.14" +requires-python = ">=3.9,<3.15" authors = [ { name = "Opentensor Foundation" }, From 22c4da2e0c7575a5bd50e8f706b4b3a8fe0a2b82 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 18:41:17 +0200 Subject: [PATCH 02/12] Adds metadata call functions retrieval --- async_substrate_interface/async_substrate.py | 41 ++++++++++++++++++-- async_substrate_interface/sync_substrate.py | 31 +++++++++++++++ async_substrate_interface/types.py | 18 +++++++++ tests/e2e_tests/test_substrate_addons.py | 4 +- 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 5fce7b8..c2342e8 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -1587,11 +1587,11 @@ async def retrieve_pending_extrinsics(self) -> list: return extrinsics async def get_metadata_storage_functions( - self, block_hash=None, runtime: Optional[Runtime] = None + self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None ) -> list: """ - Retrieves a list of all storage functions in metadata active at given block_hash (or chaintip if block_hash is - omitted) + Retrieves a list of all storage functions in metadata active at given block_hash (or chaintip if + block_hash and runtime are omitted) Args: block_hash: hash of the blockchain block whose runtime to use @@ -4079,6 +4079,41 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: return result + async def get_metadata_call_functions( + self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None + ): + """ + Retrieves calls functions for the metadata at the specified block_hash or runtime. If neither are specified, + the metadata at chaintip is used. + + Args: + block_hash: block hash to retrieve metadata for, unused if supplying runtime + runtime: Runtime object containing the metadata you wish to parse + + Returns: + dict mapping {pallet name: {call name: {param name: param definition}}} + e.g. + { + "Sudo":{ + "sudo": { + "_docs": "Authenticates the sudo key and dispatches a function call with `Root` origin.", + "call": { + "name": "call", + "type": 227, + "typeName": "Box<::RuntimeCall>", + "index": 0, + "_docs": "" + } + }, + ... + }, + ... + } + """ + if runtime is None: + runtime = await self.init_runtime(block_hash=block_hash) + return self._get_metadata_call_functions(runtime) + async def get_metadata_call_function( self, module_name: str, diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 5349b31..373e8cd 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -3291,6 +3291,37 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: return result + def get_metadata_call_functions(self, block_hash: Optional[str] = None): + """ + Retrieves calls functions for the metadata at the specified block_hash. If not specified, the metadata at + chaintip is used. + + Args: + block_hash: block hash to retrieve metadata for + + Returns: + dict mapping {pallet name: {call name: {param name: param definition}}} + e.g. + { + "Sudo":{ + "sudo": { + "_docs": "Authenticates the sudo key and dispatches a function call with `Root` origin.", + "call": { + "name": "call", + "type": 227, + "typeName": "Box<::RuntimeCall>", + "index": 0, + "_docs": "" + } + }, + ... + }, + ... + } + """ + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_call_functions(runtime) + def get_metadata_call_function( self, module_name: str, diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index f1b4917..008dd48 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -1002,3 +1002,21 @@ def generate_multisig_account( ) return multi_sig_account + + @staticmethod + def _get_metadata_call_functions(runtime: Runtime): + """ + See subclass `get_metadata_call_functions` for documentation. + """ + data = {} + for pallet in runtime.metadata.pallets: + data[pallet.name] = {} + for call in pallet.calls: + data[pallet.name][call.name] = {} + data[pallet.name][call.name]["_docs"] = " ".join(call["docs"].value) + for idx, field in enumerate(call.value.get("fields", [])): + field["index"] = idx + field_docs = field["docs"] + field["_docs"] = " ".join(field_docs) + data[pallet.name][call.name][field["name"]] = field + return data diff --git a/tests/e2e_tests/test_substrate_addons.py b/tests/e2e_tests/test_substrate_addons.py index bcf8750..bd91c99 100644 --- a/tests/e2e_tests/test_substrate_addons.py +++ b/tests/e2e_tests/test_substrate_addons.py @@ -126,9 +126,7 @@ async def test_retry_async_substrate_runtime_call_with_keyword_args(): def test_retry_sync_substrate_runtime_call_with_keyword_args(): """Test that runtime_call works with keyword arguments (parameter name conflict fix).""" - with RetrySyncSubstrate( - LATENT_LITE_ENTRYPOINT, retry_forever=True - ) as substrate: + with RetrySyncSubstrate(LATENT_LITE_ENTRYPOINT, retry_forever=True) as substrate: # This should not raise TypeError due to parameter name conflict # The 'method' kwarg should not conflict with _retry's parameter result = substrate.runtime_call( From 98fe876ac59c07aa560003b06539d3ecc2f82847 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 22:12:30 +0200 Subject: [PATCH 03/12] Moved all the metadata retrievals to shared functionality in the superclass --- async_substrate_interface/async_substrate.py | 143 ++------------ async_substrate_interface/sync_substrate.py | 157 +++------------ async_substrate_interface/types.py | 195 ++++++++++++++++++- 3 files changed, 241 insertions(+), 254 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index c2342e8..4d8f236 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -1603,21 +1603,7 @@ async def get_metadata_storage_functions( if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - storage_list = [] - - for module_idx, module in enumerate(runtime.metadata.pallets): - if module.storage: - for storage in module.storage: - storage_list.append( - self.serialize_storage_item( - storage_item=storage, - module=module, - spec_version_id=runtime.runtime_version, - runtime=runtime, - ) - ) - - return storage_list + return self._get_metadata_storage_functions(runtime=runtime) async def get_metadata_storage_function( self, @@ -1662,20 +1648,7 @@ async def get_metadata_errors( if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - error_list = [] - - for module_idx, module in enumerate(runtime.metadata.pallets): - if module.errors: - for error in module.errors: - error_list.append( - self.serialize_module_error( - module=module, - error=error, - spec_version=runtime.runtime_version, - ) - ) - - return error_list + return self._get_metadata_errors(runtime=runtime) async def get_metadata_error( self, @@ -1699,16 +1672,13 @@ async def get_metadata_error( """ if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - - for module_idx, module in enumerate(runtime.metadata.pallets): - if module.name == module_name and module.errors: - for error in module.errors: - if error_name == error.name: - return error + return self._get_metadata_error( + module_name=module_name, error_name=error_name, runtime=runtime + ) async def get_metadata_runtime_call_functions( self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None - ) -> list[GenericRuntimeCallDefinition]: + ) -> list[ScaleType]: """ Get a list of available runtime API calls @@ -1717,17 +1687,7 @@ async def get_metadata_runtime_call_functions( """ if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - call_functions = [] - - for api, methods in runtime.runtime_config.type_registry["runtime_api"].items(): - for method in methods["methods"].keys(): - call_functions.append( - await self.get_metadata_runtime_call_function( - api, method, runtime=runtime - ) - ) - - return call_functions + return self._get_metadata_runtime_call_functions(runtime=runtime) async def get_metadata_runtime_call_function( self, @@ -1735,7 +1695,7 @@ async def get_metadata_runtime_call_function( method: str, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None, - ) -> GenericRuntimeCallDefinition: + ) -> ScaleType: """ Get details of a runtime API call. If not supplying `block_hash` or `runtime`, the runtime of the current block will be used. @@ -1751,28 +1711,7 @@ async def get_metadata_runtime_call_function( """ if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - - try: - runtime_call_def = runtime.runtime_config.type_registry["runtime_api"][api][ - "methods" - ][method] - runtime_call_def["api"] = api - runtime_call_def["method"] = method - runtime_api_types = runtime.runtime_config.type_registry["runtime_api"][ - api - ].get("types", {}) - except KeyError: - raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") - - # Add runtime API types to registry - runtime.runtime_config.update_type_registry_types(runtime_api_types) - - runtime_call_def_obj = await self.create_scale_object( - "RuntimeCallDefinition", runtime=runtime - ) - runtime_call_def_obj.encode(runtime_call_def) - - return runtime_call_def_obj + return self._get_metadata_runtime_call_function(api, method, runtime) async def _get_block_handler( self, @@ -3422,16 +3361,7 @@ async def get_metadata_constants(self, block_hash=None) -> list[dict]: """ runtime = await self.init_runtime(block_hash=block_hash) - - constant_list = [] - - for module_idx, module in enumerate(runtime.metadata.pallets): - for constant in module.constants or []: - constant_list.append( - self.serialize_constant(constant, module, runtime.runtime_version) - ) - - return constant_list + return self._get_metadata_constants(runtime) async def get_metadata_constant( self, @@ -3455,12 +3385,7 @@ async def get_metadata_constant( """ if runtime is None: runtime = await self.init_runtime(block_hash=block_hash) - - for module in runtime.metadata.pallets: - if module_name == module.name and module.constants: - for constant in module.constants: - if constant_name == constant.value["name"]: - return constant + return self._get_metadata_constant(module_name, constant_name, runtime) async def get_constant( self, @@ -3604,21 +3529,7 @@ async def get_metadata_modules(self, block_hash=None) -> list[dict[str, Any]]: List of metadata modules """ runtime = await self.init_runtime(block_hash=block_hash) - - return [ - { - "metadata_index": idx, - "module_id": module.get_identifier(), - "name": module.name, - "spec_version": runtime.runtime_version, - "count_call_functions": len(module.calls or []), - "count_storage_functions": len(module.storage or []), - "count_events": len(module.events or []), - "count_constants": len(module.constants or []), - "count_errors": len(module.errors or []), - } - for idx, module in enumerate(runtime.metadata.pallets) - ] + return self._get_metadata_modules(runtime) async def get_metadata_module(self, name, block_hash=None) -> ScaleType: """ @@ -4081,7 +3992,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: async def get_metadata_call_functions( self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None - ): + ) -> dict[str, dict[str, dict[str, dict[str, Union[str, int, list]]]]]: """ Retrieves calls functions for the metadata at the specified block_hash or runtime. If neither are specified, the metadata at chaintip is used. @@ -4133,12 +4044,9 @@ async def get_metadata_call_function( """ runtime = await self.init_runtime(block_hash=block_hash) - for pallet in runtime.metadata.pallets: - if pallet.name == module_name and pallet.calls: - for call in pallet.calls: - if call.name == call_function_name: - return call - return None + return self._get_metadata_call_function( + module_name, call_function_name, runtime + ) async def get_metadata_events(self, block_hash=None) -> list[dict]: """ @@ -4152,17 +4060,7 @@ async def get_metadata_events(self, block_hash=None) -> list[dict]: """ runtime = await self.init_runtime(block_hash=block_hash) - - event_list = [] - - for event_index, (module, event) in runtime.metadata.event_index.items(): - event_list.append( - self.serialize_module_event( - module, event, runtime.runtime_version, event_index - ) - ) - - return event_list + return self._get_metadata_events(runtime) async def get_metadata_event( self, module_name, event_name, block_hash=None @@ -4182,12 +4080,7 @@ async def get_metadata_event( """ runtime = await self.init_runtime(block_hash=block_hash) - - for pallet in runtime.metadata.pallets: - if pallet.name == module_name and pallet.events: - for event in pallet.events: - if event.name == event_name: - return event + return self._get_metadata_event(runtime) async def get_block_number(self, block_hash: Optional[str] = None) -> int: """Async version of `substrateinterface.base.get_block_number` method.""" diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 373e8cd..a43a4bb 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -1042,22 +1042,8 @@ def get_metadata_storage_functions(self, block_hash=None) -> list: Returns: list of storage functions """ - self.init_runtime(block_hash=block_hash) - - storage_list = [] - - for module_idx, module in enumerate(self.metadata.pallets): - if module.storage: - for storage in module.storage: - storage_list.append( - self.serialize_storage_item( - storage_item=storage, - module=module, - spec_version_id=self.runtime.runtime_version, - ) - ) - - return storage_list + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_storage_functions(runtime=runtime) def get_metadata_storage_function(self, module_name, storage_name, block_hash=None): """ @@ -1088,24 +1074,11 @@ def get_metadata_errors(self, block_hash=None) -> list[dict[str, Optional[str]]] Returns: list of errors in the metadata """ - self.init_runtime(block_hash=block_hash) - - error_list = [] - - for module_idx, module in enumerate(self.runtime.metadata.pallets): - if module.errors: - for error in module.errors: - error_list.append( - self.serialize_module_error( - module=module, - error=error, - spec_version=self.runtime.runtime_version, - ) - ) + runtime = self.init_runtime(block_hash=block_hash) - return error_list + return self._get_metadata_errors(runtime=runtime) - def get_metadata_error(self, module_name, error_name, block_hash=None): + def get_metadata_error(self, module_name: str, error_name: str, block_hash=None): """ Retrieves the details of an error for given module name, call function name and block_hash @@ -1118,37 +1091,26 @@ def get_metadata_error(self, module_name, error_name, block_hash=None): error """ - self.init_runtime(block_hash=block_hash) - - for module_idx, module in enumerate(self.runtime.metadata.pallets): - if module.name == module_name and module.errors: - for error in module.errors: - if error_name == error.name: - return error + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_error( + module_name=module_name, error_name=error_name, runtime=runtime + ) def get_metadata_runtime_call_functions( self, block_hash: Optional[str] = None - ) -> list[GenericRuntimeCallDefinition]: + ) -> list[ScaleType]: """ Get a list of available runtime API calls Returns: list of runtime call functions """ - self.init_runtime(block_hash=block_hash) - call_functions = [] - - for api, methods in self.runtime_config.type_registry["runtime_api"].items(): - for method in methods["methods"].keys(): - call_functions.append( - self.get_metadata_runtime_call_function(api, method) - ) - - return call_functions + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_runtime_call_functions(runtime=runtime) def get_metadata_runtime_call_function( self, api: str, method: str, block_hash: Optional[str] = None - ) -> GenericRuntimeCallDefinition: + ) -> ScaleType: """ Get details of a runtime API call @@ -1160,27 +1122,9 @@ def get_metadata_runtime_call_function( Returns: runtime call function """ - self.init_runtime(block_hash=block_hash) - - try: - runtime_call_def = self.runtime_config.type_registry["runtime_api"][api][ - "methods" - ][method] - runtime_call_def["api"] = api - runtime_call_def["method"] = method - runtime_api_types = self.runtime_config.type_registry["runtime_api"][ - api - ].get("types", {}) - except KeyError: - raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") - - # Add runtime API types to registry - self.runtime_config.update_type_registry_types(runtime_api_types) - - runtime_call_def_obj = self.create_scale_object("RuntimeCallDefinition") - runtime_call_def_obj.encode(runtime_call_def) + runtime = self.init_runtime(block_hash=block_hash) - return runtime_call_def_obj + return self._get_metadata_runtime_call_function(api, method, runtime) def _get_block_handler( self, @@ -2716,15 +2660,7 @@ def get_metadata_constants(self, block_hash=None) -> list[dict]: runtime = self.init_runtime(block_hash=block_hash) - constant_list = [] - - for module_idx, module in enumerate(self.metadata.pallets): - for constant in module.constants or []: - constant_list.append( - self.serialize_constant(constant, module, runtime.runtime_version) - ) - - return constant_list + return self._get_metadata_constants(runtime) def get_metadata_constant(self, module_name, constant_name, block_hash=None): """ @@ -2739,13 +2675,8 @@ def get_metadata_constant(self, module_name, constant_name, block_hash=None): Returns: MetadataModuleConstants """ - self.init_runtime(block_hash=block_hash) - - for module in self.runtime.metadata.pallets: - if module_name == module.name and module.constants: - for constant in module.constants: - if constant_name == constant.value["name"]: - return constant + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_constant(module_name, constant_name, runtime) def get_constant( self, @@ -2881,22 +2812,8 @@ def get_metadata_modules(self, block_hash=None) -> list[dict[str, Any]]: Returns: List of metadata modules """ - self.init_runtime(block_hash=block_hash) - - return [ - { - "metadata_index": idx, - "module_id": module.get_identifier(), - "name": module.name, - "spec_version": self.runtime.runtime_version, - "count_call_functions": len(module.calls or []), - "count_storage_functions": len(module.storage or []), - "count_events": len(module.events or []), - "count_constants": len(module.constants or []), - "count_errors": len(module.errors or []), - } - for idx, module in enumerate(self.metadata.pallets) - ] + runtime = self.init_runtime(block_hash=block_hash) + return self._get_metadata_modules(runtime) def get_metadata_module(self, name, block_hash=None) -> ScaleType: """ @@ -3291,7 +3208,9 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: return result - def get_metadata_call_functions(self, block_hash: Optional[str] = None): + def get_metadata_call_functions( + self, block_hash: Optional[str] = None + ) -> dict[str, dict[str, dict[str, dict[str, Union[str, int, list]]]]]: """ Retrieves calls functions for the metadata at the specified block_hash. If not specified, the metadata at chaintip is used. @@ -3339,14 +3258,11 @@ def get_metadata_call_function( Returns: The dict-like call definition, if found. None otherwise. """ - self.init_runtime(block_hash=block_hash) + runtime = self.init_runtime(block_hash=block_hash) - for pallet in self.runtime.metadata.pallets: - if pallet.name == module_name and pallet.calls: - for call in pallet.calls: - if call.name == call_function_name: - return call - return None + return self._get_metadata_call_function( + module_name, call_function_name, runtime + ) def get_metadata_events(self, block_hash=None) -> list[dict]: """ @@ -3360,17 +3276,7 @@ def get_metadata_events(self, block_hash=None) -> list[dict]: """ runtime = self.init_runtime(block_hash=block_hash) - - event_list = [] - - for event_index, (module, event) in self.metadata.event_index.items(): - event_list.append( - self.serialize_module_event( - module, event, runtime.runtime_version, event_index - ) - ) - - return event_list + return self._get_metadata_events(runtime) def get_metadata_event( self, module_name, event_name, block_hash=None @@ -3390,12 +3296,7 @@ def get_metadata_event( """ runtime = self.init_runtime(block_hash=block_hash) - - for pallet in runtime.metadata.pallets: - if pallet.name == module_name and pallet.events: - for event in pallet.events: - if event.name == event_name: - return event + return self._get_metadata_event(runtime) def get_block_number(self, block_hash: Optional[str] = None) -> int: """Async version of `substrateinterface.base.get_block_number` method.""" diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index 008dd48..b6c6179 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -1004,7 +1004,9 @@ def generate_multisig_account( return multi_sig_account @staticmethod - def _get_metadata_call_functions(runtime: Runtime): + def _get_metadata_call_functions( + runtime: Runtime, + ) -> dict[str, dict[str, dict[str, dict[str, Union[str, int, list]]]]]: """ See subclass `get_metadata_call_functions` for documentation. """ @@ -1020,3 +1022,194 @@ def _get_metadata_call_functions(runtime: Runtime): field["_docs"] = " ".join(field_docs) data[pallet.name][call.name][field["name"]] = field return data + + @staticmethod + def _get_metadata_call_function( + module_name: str, call_function_name: str, runtime: Runtime + ): + """ + See subclass `get_metadata_call_function` for documentation. + """ + for pallet in runtime.metadata.pallets: + if pallet.name == module_name and pallet.calls: + for call in pallet.calls: + if call.name == call_function_name: + return call + return None + + @staticmethod + def _get_metadata_events(runtime: Runtime) -> list[dict]: + """ + See subclass `get_metadata_events` for documentation. + """ + event_list = [] + + for event_index, (module, event) in runtime.metadata.event_index.items(): + event_list.append( + self.serialize_module_event( + module, event, runtime.runtime_version, event_index + ) + ) + + return event_list + + @staticmethod + def _get_metadata_event(runtime: Runtime) -> Optional[Any]: + """ + See subclass `get_metadata_event` for documentation. + """ + for pallet in runtime.metadata.pallets: + if pallet.name == module_name and pallet.events: + for event in pallet.events: + if event.name == event_name: + return event + return None + + @staticmethod + def _get_metadata_constants(runtime: Runtime) -> list[dict]: + """ + See subclass `get_metadata_constants` for documentation. + """ + constant_list = [] + + for module_idx, module in enumerate(runtime.metadata.pallets): + for constant in module.constants or []: + constant_list.append( + self.serialize_constant(constant, module, runtime.runtime_version) + ) + + return constant_list + + @staticmethod + def _get_metadata_constant( + module_name: str, constant_name: str, runtime: Runtime + ) -> Optional[dict]: + """ + See subclass `get_metadata_constant` for documentation. + """ + for module in runtime.metadata.pallets: + if module_name == module.name and module.constants: + for constant in module.constants: + if constant_name == constant.value["name"]: + return constant + return None + + @staticmethod + def _get_metadata_modules(runtime: Runtime) -> list[dict[str, Any]]: + """ + See subclass `get_metadata_modules` for documentation. + """ + return [ + { + "metadata_index": idx, + "module_id": module.get_identifier(), + "name": module.name, + "spec_version": runtime.runtime_version, + "count_call_functions": len(module.calls or []), + "count_storage_functions": len(module.storage or []), + "count_events": len(module.events or []), + "count_constants": len(module.constants or []), + "count_errors": len(module.errors or []), + } + for idx, module in enumerate(runtime.metadata.pallets) + ] + + @staticmethod + def _get_metadata_storage_functions(runtime: Runtime) -> list: + """ + See subclass `get_metadata_storage_functions` for documentation. + """ + storage_list = [] + + for module_idx, module in enumerate(runtime.metadata.pallets): + if module.storage: + for storage in module.storage: + storage_list.append( + self.serialize_storage_item( + storage_item=storage, + module=module, + spec_version_id=self.runtime.runtime_version, + ) + ) + + return storage_list + + @staticmethod + def _get_metadata_errors(runtime: Runtime) -> list[dict[str, Optional[str]]]: + """ + See subclass `get_metadata_errors` for documentation. + """ + error_list = [] + + for module_idx, module in enumerate(runtime.metadata.pallets): + if module.errors: + for error in module.errors: + error_list.append( + self.serialize_module_error( + module=module, + error=error, + spec_version=runtime.runtime_version, + ) + ) + + return error_list + + @staticmethod + def _get_metadata_error( + module_name: str, error_name: str, runtime: Runtime + ) -> Optional[ScaleType]: + """ + See subclass `get_metadata_error` for documentation. + """ + for module_idx, module in enumerate(runtime.metadata.pallets): + if module.name == module_name and module.errors: + for error in module.errors: + if error_name == error.name: + return error + return None + + @staticmethod + def _get_metadata_runtime_call_function( + api: str, method: str, runtime: Runtime + ) -> scalecodec.ScaleType: + """ + See subclass `get_metadata_runtime_call_function` for documentation. + """ + try: + runtime_call_def = runtime.runtime_config.type_registry["runtime_api"][api][ + "methods" + ][method] + runtime_call_def["api"] = api + runtime_call_def["method"] = method + runtime_api_types = runtime.runtime_config.type_registry["runtime_api"][ + api + ].get("types", {}) + except KeyError: + raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") + + # Add runtime API types to registry + runtime.runtime_config.update_type_registry_types(runtime_api_types) + runtime_call_def_obj = runtime.runtime_config.create_scale_object( + "RuntimeCallDefinition" + ) + runtime_call_def_obj.encode(runtime_call_def) + + return runtime_call_def_obj + + def _get_metadata_runtime_call_functions( + self, runtime: Runtime + ) -> list[scalecodec.ScaleType]: + """ + See subclass `get_metadata_runtime_call_functions` for documentation. + """ + call_functions = [] + + for api, methods in runtime.runtime_config.type_registry["runtime_api"].items(): + for method in methods["methods"].keys(): + call_functions.append( + self._get_metadata_runtime_call_function( + api=api, method=method, runtime=runtime + ) + ) + + return call_functions From e1e37b353939a2b5adaa7ebdacfafc071f2c93be Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 22:39:58 +0200 Subject: [PATCH 04/12] Types! --- async_substrate_interface/async_substrate.py | 13 ++++---- async_substrate_interface/sync_substrate.py | 19 +++++++----- async_substrate_interface/types.py | 31 ++++++++++---------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 4d8f236..3fe188e 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -23,6 +23,7 @@ cast, ) +import scalecodec import websockets.exceptions from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string from scalecodec import GenericVariant @@ -1588,7 +1589,7 @@ async def retrieve_pending_extrinsics(self) -> list: async def get_metadata_storage_functions( self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None - ) -> list: + ) -> list[dict[str, Any]]: """ Retrieves a list of all storage functions in metadata active at given block_hash (or chaintip if block_hash and runtime are omitted) @@ -1656,7 +1657,7 @@ async def get_metadata_error( error_name: str, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None, - ): + ) -> Optional[scalecodec.GenericVariant]: """ Retrieves the details of an error for given module name, call function name and block_hash @@ -1678,7 +1679,7 @@ async def get_metadata_error( async def get_metadata_runtime_call_functions( self, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None - ) -> list[ScaleType]: + ) -> list[scalecodec.GenericRuntimeCallDefinition]: """ Get a list of available runtime API calls @@ -1695,7 +1696,7 @@ async def get_metadata_runtime_call_function( method: str, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None, - ) -> ScaleType: + ) -> scalecodec.GenericRuntimeCallDefinition: """ Get details of a runtime API call. If not supplying `block_hash` or `runtime`, the runtime of the current block will be used. @@ -3369,7 +3370,7 @@ async def get_metadata_constant( constant_name: str, block_hash: Optional[str] = None, runtime: Optional[Runtime] = None, - ): + ) -> Optional[scalecodec.ScaleInfoModuleConstantMetadata]: """ Retrieves the details of a constant for given module name, call function name and block_hash (or chaintip if block_hash is omitted) @@ -4080,7 +4081,7 @@ async def get_metadata_event( """ runtime = await self.init_runtime(block_hash=block_hash) - return self._get_metadata_event(runtime) + return self._get_metadata_event(module_name, event_name, runtime) async def get_block_number(self, block_hash: Optional[str] = None) -> int: """Async version of `substrateinterface.base.get_block_number` method.""" diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index a43a4bb..fb9fad9 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -6,6 +6,7 @@ from typing import Optional, Union, Callable, Any from unittest.mock import MagicMock +import scalecodec from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string from scalecodec import ( GenericCall, @@ -1031,7 +1032,7 @@ def retrieve_pending_extrinsics(self) -> list: return extrinsics - def get_metadata_storage_functions(self, block_hash=None) -> list: + def get_metadata_storage_functions(self, block_hash=None) -> list[dict[str, Any]]: """ Retrieves a list of all storage functions in metadata active at given block_hash (or chaintip if block_hash is omitted) @@ -1078,7 +1079,9 @@ def get_metadata_errors(self, block_hash=None) -> list[dict[str, Optional[str]]] return self._get_metadata_errors(runtime=runtime) - def get_metadata_error(self, module_name: str, error_name: str, block_hash=None): + def get_metadata_error( + self, module_name: str, error_name: str, block_hash=None + ) -> Optional[scalecodec.GenericVariant]: """ Retrieves the details of an error for given module name, call function name and block_hash @@ -1098,7 +1101,7 @@ def get_metadata_error(self, module_name: str, error_name: str, block_hash=None) def get_metadata_runtime_call_functions( self, block_hash: Optional[str] = None - ) -> list[ScaleType]: + ) -> list[scalecodec.GenericRuntimeCallDefinition]: """ Get a list of available runtime API calls @@ -1110,7 +1113,7 @@ def get_metadata_runtime_call_functions( def get_metadata_runtime_call_function( self, api: str, method: str, block_hash: Optional[str] = None - ) -> ScaleType: + ) -> scalecodec.GenericRuntimeCallDefinition: """ Get details of a runtime API call @@ -2662,7 +2665,9 @@ def get_metadata_constants(self, block_hash=None) -> list[dict]: return self._get_metadata_constants(runtime) - def get_metadata_constant(self, module_name, constant_name, block_hash=None): + def get_metadata_constant( + self, module_name, constant_name, block_hash=None + ) -> Optional[scalecodec.ScaleInfoModuleConstantMetadata]: """ Retrieves the details of a constant for given module name, call function name and block_hash (or chaintip if block_hash is omitted) @@ -3279,7 +3284,7 @@ def get_metadata_events(self, block_hash=None) -> list[dict]: return self._get_metadata_events(runtime) def get_metadata_event( - self, module_name, event_name, block_hash=None + self, module_name: str, event_name: str, block_hash=None ) -> Optional[Any]: """ Retrieves the details of an event for given module name, call function name and block_hash @@ -3296,7 +3301,7 @@ def get_metadata_event( """ runtime = self.init_runtime(block_hash=block_hash) - return self._get_metadata_event(runtime) + return self._get_metadata_event(module_name, event_name, runtime) def get_block_number(self, block_hash: Optional[str] = None) -> int: """Async version of `substrateinterface.base.get_block_number` method.""" diff --git a/async_substrate_interface/types.py b/async_substrate_interface/types.py index b6c6179..a627132 100644 --- a/async_substrate_interface/types.py +++ b/async_substrate_interface/types.py @@ -676,8 +676,8 @@ def is_valid_ss58_address(self, value: str) -> bool: def serialize_storage_item( self, - storage_item: ScaleType, - module: str, + storage_item: scalecodec.ScaleInfoStorageEntryMetadata, + module: scalecodec.ScaleInfoPalletMetadata, spec_version_id: int, runtime: Optional[Runtime] = None, ) -> dict: @@ -1026,7 +1026,7 @@ def _get_metadata_call_functions( @staticmethod def _get_metadata_call_function( module_name: str, call_function_name: str, runtime: Runtime - ): + ) -> Optional[scalecodec.GenericVariant]: """ See subclass `get_metadata_call_function` for documentation. """ @@ -1037,8 +1037,7 @@ def _get_metadata_call_function( return call return None - @staticmethod - def _get_metadata_events(runtime: Runtime) -> list[dict]: + def _get_metadata_events(self, runtime: Runtime) -> list[dict]: """ See subclass `get_metadata_events` for documentation. """ @@ -1054,7 +1053,9 @@ def _get_metadata_events(runtime: Runtime) -> list[dict]: return event_list @staticmethod - def _get_metadata_event(runtime: Runtime) -> Optional[Any]: + def _get_metadata_event( + module_name: str, event_name: str, runtime: Runtime + ) -> Optional[scalecodec.GenericScaleInfoEvent]: """ See subclass `get_metadata_event` for documentation. """ @@ -1065,8 +1066,7 @@ def _get_metadata_event(runtime: Runtime) -> Optional[Any]: return event return None - @staticmethod - def _get_metadata_constants(runtime: Runtime) -> list[dict]: + def _get_metadata_constants(self, runtime: Runtime) -> list[dict]: """ See subclass `get_metadata_constants` for documentation. """ @@ -1083,7 +1083,7 @@ def _get_metadata_constants(runtime: Runtime) -> list[dict]: @staticmethod def _get_metadata_constant( module_name: str, constant_name: str, runtime: Runtime - ) -> Optional[dict]: + ) -> Optional[scalecodec.ScaleInfoModuleConstantMetadata]: """ See subclass `get_metadata_constant` for documentation. """ @@ -1114,8 +1114,7 @@ def _get_metadata_modules(runtime: Runtime) -> list[dict[str, Any]]: for idx, module in enumerate(runtime.metadata.pallets) ] - @staticmethod - def _get_metadata_storage_functions(runtime: Runtime) -> list: + def _get_metadata_storage_functions(self, runtime: Runtime) -> list[dict[str, Any]]: """ See subclass `get_metadata_storage_functions` for documentation. """ @@ -1129,13 +1128,13 @@ def _get_metadata_storage_functions(runtime: Runtime) -> list: storage_item=storage, module=module, spec_version_id=self.runtime.runtime_version, + runtime=runtime, ) ) return storage_list - @staticmethod - def _get_metadata_errors(runtime: Runtime) -> list[dict[str, Optional[str]]]: + def _get_metadata_errors(self, runtime: Runtime) -> list[dict[str, Optional[str]]]: """ See subclass `get_metadata_errors` for documentation. """ @@ -1157,7 +1156,7 @@ def _get_metadata_errors(runtime: Runtime) -> list[dict[str, Optional[str]]]: @staticmethod def _get_metadata_error( module_name: str, error_name: str, runtime: Runtime - ) -> Optional[ScaleType]: + ) -> Optional[scalecodec.GenericVariant]: """ See subclass `get_metadata_error` for documentation. """ @@ -1171,7 +1170,7 @@ def _get_metadata_error( @staticmethod def _get_metadata_runtime_call_function( api: str, method: str, runtime: Runtime - ) -> scalecodec.ScaleType: + ) -> scalecodec.GenericRuntimeCallDefinition: """ See subclass `get_metadata_runtime_call_function` for documentation. """ @@ -1198,7 +1197,7 @@ def _get_metadata_runtime_call_function( def _get_metadata_runtime_call_functions( self, runtime: Runtime - ) -> list[scalecodec.ScaleType]: + ) -> list[scalecodec.GenericRuntimeCallDefinition]: """ See subclass `get_metadata_runtime_call_functions` for documentation. """ From a688e481b54688e71f4a684c786446303ce837cd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 24 Oct 2025 23:09:25 +0200 Subject: [PATCH 05/12] Updates get_payment_info to include all the params of the underlying create_signed_extrinsic --- async_substrate_interface/async_substrate.py | 21 ++++++++++++++++-- async_substrate_interface/sync_substrate.py | 23 ++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index c2342e8..6ff837b 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -3500,7 +3500,13 @@ async def get_constant( return None async def get_payment_info( - self, call: GenericCall, keypair: Keypair + self, + call: GenericCall, + keypair: Keypair, + era: Optional[Union[dict, str]] = None, + nonce: Optional[int] = None, + tip: int = 0, + tip_asset_id: Optional[int] = None, ) -> dict[str, Any]: """ Retrieves fee estimation via RPC for given extrinsic @@ -3509,6 +3515,11 @@ async def get_payment_info( call: Call object to estimate fees for keypair: Keypair of the sender, does not have to include private key because no valid signature is required + era: Specify mortality in blocks in follow format: + {'period': [amount_blocks]} If omitted the extrinsic is immortal + nonce: nonce to include in extrinsics, if omitted the current nonce is retrieved on-chain + tip: The tip for the block author to gain priority during network congestion + tip_asset_id: Optional asset ID with which to pay the tip Returns: Dict with payment info @@ -3528,7 +3539,13 @@ async def get_payment_info( # Create extrinsic extrinsic = await self.create_signed_extrinsic( - call=call, keypair=keypair, signature=signature + call=call, + keypair=keypair, + era=era, + nonce=nonce, + tip=tip, + tip_asset_id=tip_asset_id, + signature=signature, ) extrinsic_len = len(extrinsic.data) diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index 373e8cd..7f233eb 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -2779,7 +2779,15 @@ def get_constant( else: return None - def get_payment_info(self, call: GenericCall, keypair: Keypair) -> dict[str, Any]: + def get_payment_info( + self, + call: GenericCall, + keypair: Keypair, + era: Optional[Union[dict, str]] = None, + nonce: Optional[int] = None, + tip: int = 0, + tip_asset_id: Optional[int] = None, + ) -> dict[str, Any]: """ Retrieves fee estimation via RPC for given extrinsic @@ -2787,6 +2795,11 @@ def get_payment_info(self, call: GenericCall, keypair: Keypair) -> dict[str, Any call: Call object to estimate fees for keypair: Keypair of the sender, does not have to include private key because no valid signature is required + era: Specify mortality in blocks in follow format: + {'period': [amount_blocks]} If omitted the extrinsic is immortal + nonce: nonce to include in extrinsics, if omitted the current nonce is retrieved on-chain + tip: The tip for the block author to gain priority during network congestion + tip_asset_id: Optional asset ID with which to pay the tip Returns: Dict with payment info @@ -2806,7 +2819,13 @@ def get_payment_info(self, call: GenericCall, keypair: Keypair) -> dict[str, Any # Create extrinsic extrinsic = self.create_signed_extrinsic( - call=call, keypair=keypair, signature=signature + call=call, + keypair=keypair, + era=era, + nonce=nonce, + tip=tip, + tip_asset_id=tip_asset_id, + signature=signature, ) extrinsic_len = len(extrinsic.data) From 77c80d645140258c2eeb72e7e49e9e7bbe7a5d91 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 27 Oct 2025 23:29:03 +0200 Subject: [PATCH 06/12] Adds tests --- .../test_async_substrate_interface.py | 40 +++++++++++++++++++ .../test_substrate_interface.py | 39 ++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/tests/integration_tests/test_async_substrate_interface.py b/tests/integration_tests/test_async_substrate_interface.py index 8957314..1acbed6 100644 --- a/tests/integration_tests/test_async_substrate_interface.py +++ b/tests/integration_tests/test_async_substrate_interface.py @@ -4,6 +4,7 @@ import time import threading +import bittensor_wallet import pytest from scalecodec import ss58_encode @@ -235,3 +236,42 @@ async def test_improved_reconnection(): shutdown_thread.start() shutdown_thread.join(timeout=5) server_thread.join(timeout=5) + + +@pytest.mark.asyncio +async def test_get_payment_info(): + alice_coldkey = bittensor_wallet.Keypair.create_from_uri("//Alice") + bob_coldkey = bittensor_wallet.Keypair.create_from_uri("//Bob") + async with AsyncSubstrateInterface( + LATENT_LITE_ENTRYPOINT, ss58_format=42, chain_name="Bittensor" + ) as substrate: + block_hash = await substrate.get_chain_head() + call = await substrate.compose_call( + "Balances", + "transfer_keep_alive", + {"dest": bob_coldkey.ss58_address, "value": 100_000}, + block_hash, + ) + payment_info = await substrate.get_payment_info( + call=call, + keypair=alice_coldkey, + ) + partial_fee_no_era = payment_info["partial_fee"] + assert partial_fee_no_era > 0 + payment_info_era = await substrate.get_payment_info( + call=call, keypair=alice_coldkey, era={"period": 64} + ) + partial_fee_era = payment_info_era["partial_fee"] + assert partial_fee_era > partial_fee_no_era + + payment_info_all_options = await substrate.get_payment_info( + call=call, + keypair=alice_coldkey, + era={"period": 64}, + nonce=await substrate.get_account_nonce(alice_coldkey.ss58_address), + tip=5_000_000, + tip_asset_id=64, + ) + partial_fee_all_options = payment_info_all_options["partial_fee"] + assert partial_fee_all_options > partial_fee_no_era + assert partial_fee_all_options > partial_fee_era diff --git a/tests/integration_tests/test_substrate_interface.py b/tests/integration_tests/test_substrate_interface.py index ad80401..42885ac 100644 --- a/tests/integration_tests/test_substrate_interface.py +++ b/tests/integration_tests/test_substrate_interface.py @@ -1,3 +1,4 @@ +import bittensor_wallet from scalecodec import ss58_encode from async_substrate_interface.sync_substrate import SubstrateInterface @@ -112,3 +113,41 @@ def test_query_map_with_odd_number_of_params(): first_record = qm.records[0] assert len(first_record) == 2 assert len(first_record[0]) == 4 + + +def test_get_payment_info(): + alice_coldkey = bittensor_wallet.Keypair.create_from_uri("//Alice") + bob_coldkey = bittensor_wallet.Keypair.create_from_uri("//Bob") + with SubstrateInterface( + LATENT_LITE_ENTRYPOINT, ss58_format=42, chain_name="Bittensor" + ) as substrate: + block_hash = substrate.get_chain_head() + call = substrate.compose_call( + "Balances", + "transfer_keep_alive", + {"dest": bob_coldkey.ss58_address, "value": 100_000}, + block_hash, + ) + payment_info = substrate.get_payment_info( + call=call, + keypair=alice_coldkey, + ) + partial_fee_no_era = payment_info["partial_fee"] + assert partial_fee_no_era > 0 + payment_info_era = substrate.get_payment_info( + call=call, keypair=alice_coldkey, era={"period": 64} + ) + partial_fee_era = payment_info_era["partial_fee"] + assert partial_fee_era > partial_fee_no_era + + payment_info_all_options = substrate.get_payment_info( + call=call, + keypair=alice_coldkey, + era={"period": 64}, + nonce=substrate.get_account_nonce(alice_coldkey.ss58_address), + tip=5_000_000, + tip_asset_id=64, + ) + partial_fee_all_options = payment_info_all_options["partial_fee"] + assert partial_fee_all_options > partial_fee_no_era + assert partial_fee_all_options > partial_fee_era From 4754fed9481d9932f472ad3028f3b66fe05562e2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 27 Oct 2025 23:29:44 +0200 Subject: [PATCH 07/12] Dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ddb2b89..4a1eec9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,5 +56,6 @@ dev = [ "pytest-split==0.10.0", "pytest-xdist==3.6.1", "pytest-rerunfailures==10.2", - "substrate-interface" + "substrate-interface", + "bittensor-wallet>=0.6" ] From 16ef73015f45885c9273d0e51387971f6b1a08bf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 27 Oct 2025 23:30:06 +0200 Subject: [PATCH 08/12] Dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a1eec9..3bf6bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,5 +57,5 @@ dev = [ "pytest-xdist==3.6.1", "pytest-rerunfailures==10.2", "substrate-interface", - "bittensor-wallet>=0.6" + "bittensor-wallet>=4.0.0" ] From 98a6d5ecc64bd83cb66a70a9e2266f0c3c36e66f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 17:13:16 +0200 Subject: [PATCH 09/12] Drop legacy substrateinterface compatibility tests --- pyproject.toml | 1 - tests/e2e_tests/test_old_new.py | 150 -------------------------------- 2 files changed, 151 deletions(-) delete mode 100644 tests/e2e_tests/test_old_new.py diff --git a/pyproject.toml b/pyproject.toml index bc9e6f8..398fcc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,5 +56,4 @@ dev = [ "pytest-split==0.10.0", "pytest-xdist==3.6.1", "pytest-rerunfailures==10.2", - "substrate-interface" ] diff --git a/tests/e2e_tests/test_old_new.py b/tests/e2e_tests/test_old_new.py deleted file mode 100644 index c0a85e4..0000000 --- a/tests/e2e_tests/test_old_new.py +++ /dev/null @@ -1,150 +0,0 @@ -import asyncio -import os -import time - -import bittensor as bt -from bittensor.core.chain_data import decode_account_id -from bittensor.core.settings import SS58_FORMAT -import pytest -import substrateinterface - -from async_substrate_interface.async_substrate import AsyncSubstrateInterface -from async_substrate_interface.sync_substrate import SubstrateInterface - -try: - n = int(os.getenv("NUMBER_RUNS")) -except TypeError: - n = 3 - -FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" -coldkey = "5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn" - -# dtao epoch is 4920350 - -b_pre = 4920340 -b_post = 4920360 - - -@pytest.mark.asyncio -async def test_async(): - async with bt.async_subtensor("archive") as st: - print("ss58 format:", st.substrate.ss58_format) - print("current block (async):", await st.block) - for i in range(n): - s0 = await st.get_stake_for_coldkey(coldkey, block=b_post + i) - print(f"at block {b_post + i}: {s0}") - for i in range(n): - s1 = ( - await st.query_subtensor( - "TotalColdkeyStake", block=b_pre + i, params=[coldkey] - ) - ).value - print(f"at block {b_pre + i}: {s1}") - for i in range(n): - s2 = await st.get_stake_for_coldkey(coldkey, block=b_post + i) - print(f"at block {b_post + i}: {s2}") - - -def test_sync(): - with bt.subtensor("archive") as st: - print("ss58 format:", st.substrate.ss58_format) - print("current block (sync):", st.block) - for i in range(n): - s0 = st.get_stake_for_coldkey(coldkey, block=b_post + i) - print(f"at block {b_post + i}: {s0}") - for i in range(n): - s1 = st.query_subtensor("TotalColdkeyStake", b_pre + i, [coldkey]).value - print(f"at block {b_pre + i}: {s1}") - for i in range(n): - s2 = st.get_stake_for_coldkey(coldkey, block=b_post + i) - print(f"at block {b_post + i}: {s2}") - - -@pytest.mark.asyncio -async def test_query_map(): - async def async_gathering(): - async def exhaust(qmr): - r = [] - async for k, v in await qmr: - r.append((k, v)) - return r - - start = time.time() - async with AsyncSubstrateInterface( - FINNEY_ENTRYPOINT, ss58_format=SS58_FORMAT - ) as substrate: - block_hash = await substrate.get_chain_head() - tasks = [ - substrate.query_map( - "SubtensorModule", - "TaoDividendsPerSubnet", - [netuid], - block_hash=block_hash, - ) - for netuid in range(1, 51) - ] - tasks = [exhaust(task) for task in tasks] - print(time.time() - start) - results_dicts_list = [] - for future in asyncio.as_completed(tasks): - result = await future - results_dicts_list.extend( - [(decode_account_id(k), v.value) for k, v in result] - ) - - elapsed = time.time() - start - print(f"Async Time: {elapsed}") - - print("Async Results", len(results_dicts_list)) - return results_dicts_list, block_hash - - def sync_new_method(block_hash): - result_dicts_list = [] - start = time.time() - with SubstrateInterface( - FINNEY_ENTRYPOINT, ss58_format=SS58_FORMAT - ) as substrate: - for netuid in range(1, 51): - tao_divs = list( - substrate.query_map( - "SubtensorModule", - "TaoDividendsPerSubnet", - [netuid], - block_hash=block_hash, - ) - ) - tao_divs = [(decode_account_id(k), v.value) for k, v in tao_divs] - result_dicts_list.extend(tao_divs) - print("New Sync Time:", time.time() - start) - print("New Sync Results", len(result_dicts_list)) - return result_dicts_list - - def sync_old_method(block_hash): - results_dicts_list = [] - start = time.time() - substrate = substrateinterface.SubstrateInterface( - FINNEY_ENTRYPOINT, ss58_format=SS58_FORMAT - ) - for netuid in range(1, 51): - tao_divs = list( - substrate.query_map( - "SubtensorModule", - "TaoDividendsPerSubnet", - [netuid], - block_hash=block_hash, - ) - ) - tao_divs = [(k.value, v.value) for k, v in tao_divs] - results_dicts_list.extend(tao_divs) - substrate.close() - print("Legacy Sync Time:", time.time() - start) - print("Legacy Sync Results", len(results_dicts_list)) - return results_dicts_list - - async_, block_hash_ = await async_gathering() - new_sync_ = sync_new_method(block_hash_) - legacy_sync = sync_old_method(block_hash_) - for k_v in async_: - assert k_v in legacy_sync - for k_v in new_sync_: - assert k_v in legacy_sync From 6889ddef83345d3326ff5d5fbd85c1917af5adb5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 23:17:54 +0200 Subject: [PATCH 10/12] bump bt-decode --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 398fcc3..8eddece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ keywords = ["substrate", "development", "bittensor"] dependencies = [ "wheel", - "bt-decode==v0.6.0", + "bt-decode==v0.8.0", "scalecodec~=1.2.11", "websockets>=14.1", "xxhash", From 13c4bfa1c72f60e5e7bc238e3db5d8a5940e2c6d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 29 Oct 2025 15:36:17 +0200 Subject: [PATCH 11/12] Updates workflow to work better with community PRs --- .github/workflows/check-sdk-tests.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-sdk-tests.yml b/.github/workflows/check-sdk-tests.yml index 7529ea3..474c84a 100644 --- a/.github/workflows/check-sdk-tests.yml +++ b/.github/workflows/check-sdk-tests.yml @@ -263,15 +263,12 @@ jobs: python3 -m pip install --upgrade pip uv uv pip install '.[dev]' - - name: Clone async-substrate-interface repo - run: git clone https://github.com/opentensor/async-substrate-interface.git - - name: Checkout PR branch in async-substrate-interface repo - working-directory: ${{ github.workspace }}/async-substrate-interface - run: | - git fetch origin ${{ github.event.pull_request.head.ref }} - git checkout ${{ github.event.pull_request.head.ref }} - echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + path: async-substrate-interface - name: Install /async-substrate-interface package working-directory: ${{ github.workspace }}/async-substrate-interface From de18320041e798a27e655a469f070ddba40cc191 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 29 Oct 2025 23:03:29 +0200 Subject: [PATCH 12/12] Changelog + version --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf50b7..70d8c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ # Changelog +## 1.5.9 /2025-10-29 +* Adds metadata call functions retrieval by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/223 +* move metadata methods to SubstrateMixin by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/224 +* Update get_payment_info to include addl params by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/226 +* Python 3.14 by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/228 +* Support python 3.14 by @Moisan in https://github.com/opentensor/async-substrate-interface/pull/210 + +## New Contributors +* @Moisan made their first contribution in https://github.com/opentensor/async-substrate-interface/pull/210 + +**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.8...v1.5.9 + ## 1.5.8 /2025-10-21 * Fix parameter name conflict in retry substrate _retry() methods by @Arthurdw in https://github.com/opentensor/async-substrate-interface/pull/218 * Use uv for test dependencies by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/219 diff --git a/pyproject.toml b/pyproject.toml index 94bac0d..99fe2df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "async-substrate-interface" -version = "1.5.8" +version = "1.5.9" description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface" readme = "README.md" license = { file = "LICENSE" }