Skip to content

Commit 75e50f7

Browse files
authored
Merge pull request #21 from opentensor/release/1.0.0rc7
Release/1.0.0rc7
2 parents 3c209e4 + a4d9ba5 commit 75e50f7

File tree

8 files changed

+295
-150
lines changed

8 files changed

+295
-150
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.0.0rc7 /2025-01-29
4+
5+
## What's Changed
6+
* feat: use bt_decode in runtime_call by @zyzniewski-reef in https://github.com/opentensor/async-substrate-interface/pull/15
7+
* Move logic to mixin + fix tests by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/18
8+
* Fix decode scale by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/19
9+
* Backmerge main to staging rc5 by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/20
10+
311
## 1.0.0rc6 /2025-01-28
412

513
## What's Changed

async_substrate_interface/async_substrate.py

+86-88
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@
2323

2424
import asyncstdlib as a
2525
from bittensor_wallet.keypair import Keypair
26-
from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15
26+
from bittensor_wallet.utils import SS58_FORMAT
27+
from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
2728
from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject
28-
from scalecodec.types import GenericCall, GenericRuntimeCallDefinition, GenericExtrinsic
29+
from scalecodec.types import (
30+
GenericCall,
31+
GenericExtrinsic,
32+
GenericRuntimeCallDefinition,
33+
ss58_decode,
34+
)
2935
from websockets.asyncio.client import connect
3036
from websockets.exceptions import ConnectionClosed
3137

@@ -789,8 +795,54 @@ async def load_registry(self):
789795
)
790796
metadata_option_hex_str = metadata_rpc_result["result"]
791797
metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:])
792-
metadata_v15 = MetadataV15.decode_from_metadata_option(metadata_option_bytes)
793-
self.registry = PortableRegistry.from_metadata_v15(metadata_v15)
798+
self.metadata_v15 = MetadataV15.decode_from_metadata_option(
799+
metadata_option_bytes
800+
)
801+
self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15)
802+
803+
async def _wait_for_registry(self, _attempt: int = 1, _retries: int = 3) -> None:
804+
async def _waiter():
805+
while self.registry is None:
806+
await asyncio.sleep(0.1)
807+
return
808+
809+
try:
810+
if not self.registry:
811+
await asyncio.wait_for(_waiter(), timeout=10)
812+
except TimeoutError:
813+
# indicates that registry was never loaded
814+
if not self._initializing:
815+
raise AttributeError(
816+
"Registry was never loaded. This did not occur during initialization, which usually indicates "
817+
"you must first initialize the AsyncSubstrateInterface object, either with "
818+
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
819+
)
820+
elif _attempt < _retries:
821+
await self.load_registry()
822+
return await self._wait_for_registry(_attempt + 1, _retries)
823+
else:
824+
raise AttributeError(
825+
"Registry was never loaded. This occurred during initialization, which usually indicates a "
826+
"connection or node error."
827+
)
828+
829+
async def encode_scale(
830+
self, type_string, value: Any, _attempt: int = 1, _retries: int = 3
831+
) -> bytes:
832+
"""
833+
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
834+
835+
Args:
836+
type_string: the type string of the SCALE object for decoding
837+
value: value to encode
838+
_attempt: the current number of attempts to load the registry needed to encode the value
839+
_retries: the maximum number of attempts to load the registry needed to encode the value
840+
841+
Returns:
842+
encoded bytes
843+
"""
844+
await self._wait_for_registry(_attempt, _retries)
845+
return self._encode_scale(type_string, value)
794846

795847
async def decode_scale(
796848
self,
@@ -799,7 +851,7 @@ async def decode_scale(
799851
_attempt=1,
800852
_retries=3,
801853
return_scale_obj=False,
802-
) -> Any:
854+
) -> Union[ScaleObj, Any]:
803855
"""
804856
Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string
805857
(e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash
@@ -815,62 +867,20 @@ async def decode_scale(
815867
Returns:
816868
Decoded object
817869
"""
818-
819-
async def _wait_for_registry():
820-
while self.registry is None:
821-
await asyncio.sleep(0.1)
822-
return
823-
824870
if scale_bytes == b"\x00":
825871
obj = None
826872
else:
827-
try:
828-
if not self.registry:
829-
await asyncio.wait_for(_wait_for_registry(), timeout=10)
873+
if type_string == "scale_info::0": # Is an AccountId
874+
# Decode AccountId bytes to SS58 address
875+
return bytes.fromhex(ss58_decode(scale_bytes, SS58_FORMAT))
876+
else:
877+
await self._wait_for_registry(_attempt, _retries)
830878
obj = decode_by_type_string(type_string, self.registry, scale_bytes)
831-
except TimeoutError:
832-
# indicates that registry was never loaded
833-
if not self._initializing:
834-
raise AttributeError(
835-
"Registry was never loaded. This did not occur during initialization, which usually indicates "
836-
"you must first initialize the AsyncSubstrateInterface object, either with "
837-
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
838-
)
839-
elif _attempt < _retries:
840-
await self.load_registry()
841-
return await self.decode_scale(
842-
type_string, scale_bytes, _attempt + 1
843-
)
844-
else:
845-
raise AttributeError(
846-
"Registry was never loaded. This occurred during initialization, which usually indicates a "
847-
"connection or node error."
848-
)
849879
if return_scale_obj:
850880
return ScaleObj(obj)
851881
else:
852882
return obj
853883

854-
async def encode_scale(self, type_string, value, block_hash=None) -> ScaleBytes:
855-
"""
856-
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
857-
858-
Args:
859-
type_string: the type string of the SCALE object for decoding
860-
value: value to encode
861-
block_hash: the hash of the blockchain block whose metadata to use for encoding
862-
863-
Returns:
864-
ScaleBytes encoded value
865-
"""
866-
if not self._metadata or block_hash:
867-
await self.init_runtime(block_hash=block_hash)
868-
869-
obj = self.runtime_config.create_scale_object(
870-
type_string=type_string, metadata=self._metadata
871-
)
872-
return obj.encode(value)
873-
874884
async def _first_initialize_runtime(self):
875885
"""
876886
TODO docstring
@@ -2173,7 +2183,7 @@ async def query_multi(
21732183
await self.decode_scale(
21742184
storage_key.value_scale_type, change_data
21752185
),
2176-
)
2186+
),
21772187
)
21782188

21792189
return result
@@ -2503,56 +2513,43 @@ async def runtime_call(
25032513
params = {}
25042514

25052515
try:
2506-
runtime_call_def = self.runtime_config.type_registry["runtime_api"][api][
2507-
"methods"
2508-
][method]
2509-
runtime_api_types = self.runtime_config.type_registry["runtime_api"][
2510-
api
2511-
].get("types", {})
2516+
metadata_v15 = self.metadata_v15.value()
2517+
apis = {entry["name"]: entry for entry in metadata_v15["apis"]}
2518+
api_entry = apis[api]
2519+
methods = {entry["name"]: entry for entry in api_entry["methods"]}
2520+
runtime_call_def = methods[method]
25122521
except KeyError:
25132522
raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry")
25142523

2515-
if isinstance(params, list) and len(params) != len(runtime_call_def["params"]):
2524+
if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]):
25162525
raise ValueError(
25172526
f"Number of parameter provided ({len(params)}) does not "
2518-
f"match definition {len(runtime_call_def['params'])}"
2527+
f"match definition {len(runtime_call_def['inputs'])}"
25192528
)
25202529

2521-
# Add runtime API types to registry
2522-
self.runtime_config.update_type_registry_types(runtime_api_types)
2523-
runtime = Runtime(
2524-
self.chain,
2525-
self.runtime_config,
2526-
self._metadata,
2527-
self.type_registry,
2528-
)
2529-
25302530
# Encode params
2531-
param_data = ScaleBytes(bytes())
2532-
for idx, param in enumerate(runtime_call_def["params"]):
2533-
scale_obj = runtime.runtime_config.create_scale_object(param["type"])
2531+
param_data = b""
2532+
for idx, param in enumerate(runtime_call_def["inputs"]):
2533+
param_type_string = f"scale_info::{param['ty']}"
25342534
if isinstance(params, list):
2535-
param_data += scale_obj.encode(params[idx])
2535+
param_data += await self.encode_scale(param_type_string, params[idx])
25362536
else:
25372537
if param["name"] not in params:
25382538
raise ValueError(f"Runtime Call param '{param['name']}' is missing")
25392539

2540-
param_data += scale_obj.encode(params[param["name"]])
2540+
param_data += await self.encode_scale(
2541+
param_type_string, params[param["name"]]
2542+
)
25412543

25422544
# RPC request
25432545
result_data = await self.rpc_request(
2544-
"state_call", [f"{api}_{method}", str(param_data), block_hash]
2546+
"state_call", [f"{api}_{method}", param_data.hex(), block_hash]
25452547
)
2548+
output_type_string = f"scale_info::{runtime_call_def['output']}"
25462549

25472550
# Decode result
2548-
# TODO update this to use bt-decode
2549-
result_obj = runtime.runtime_config.create_scale_object(
2550-
runtime_call_def["type"]
2551-
)
2552-
result_obj.decode(
2553-
ScaleBytes(result_data["result"]),
2554-
check_remaining=self.config.get("strict_scale_decode"),
2555-
)
2551+
result_bytes = hex_to_bytes(result_data["result"])
2552+
result_obj = ScaleObj(await self.decode_scale(output_type_string, result_bytes))
25562553

25572554
return result_obj
25582555

@@ -2581,7 +2578,7 @@ async def get_account_next_index(self, account_address: str) -> int:
25812578
"""
25822579
This method maintains a cache of nonces for each account ss58address.
25832580
Upon subsequent calls, it will return the cached nonce + 1 instead of fetching from the chain.
2584-
This allows for correct nonce management in-case of async context when gathering co-routines.
2581+
This allows for correct nonce management in-case of async context when gathering co-routines.
25852582
25862583
Args:
25872584
account_address: SS58 formatted address
@@ -2595,7 +2592,9 @@ async def get_account_next_index(self, account_address: str) -> int:
25952592

25962593
async with self._lock:
25972594
if self._nonces.get(account_address) is None:
2598-
nonce_obj = await self.rpc_request("account_nextIndex", [account_address])
2595+
nonce_obj = await self.rpc_request(
2596+
"account_nextIndex", [account_address]
2597+
)
25992598
self._nonces[account_address] = nonce_obj["result"]
26002599
else:
26012600
self._nonces[account_address] += 1
@@ -2686,8 +2685,7 @@ async def get_payment_info(
26862685
extrinsic = await self.create_signed_extrinsic(
26872686
call=call, keypair=keypair, signature=signature
26882687
)
2689-
extrinsic_len = self.runtime_config.create_scale_object("u32")
2690-
extrinsic_len.encode(len(extrinsic.data))
2688+
extrinsic_len = len(extrinsic.data)
26912689

26922690
result = await self.runtime_call(
26932691
"TransactionPaymentApi", "query_info", [extrinsic, extrinsic_len]

0 commit comments

Comments
 (0)