Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9ec8095
Update project name for PyPI
thewhaleking Jan 28, 2025
303f544
Merge pull request #16 from opentensor/new-project-name
ibraheem-abe Jan 28, 2025
3e245c4
feat: use bt_decode in runtime_call
zyzniewski-reef Jan 28, 2025
24f3041
fix: return ScaleObj
zyzniewski-reef Jan 29, 2025
c6597dd
fix: unneeded condition (already covered in decode_scale)
zyzniewski-reef Jan 29, 2025
99ac163
style: ruff formatting
zyzniewski-reef Jan 29, 2025
fba32e1
fix: get ScaleObj.value
zyzniewski-reef Jan 29, 2025
356eeea
fix: remove unused code
zyzniewski-reef Jan 29, 2025
c294eee
Removed superclass of ScaleObj
thewhaleking Jan 29, 2025
8890e47
Merge pull request #15 from opentensor/feat/use_bt_decode
thewhaleking Jan 29, 2025
aa7ad25
Ruff + _metadata_cache fix
thewhaleking Jan 29, 2025
e66a804
Moved the logic of encode_scale to the Mixin. Fix tests I broke.
thewhaleking Jan 29, 2025
de9cf65
Remove experimental method.
thewhaleking Jan 29, 2025
e1fa0ed
Merge pull request #18 from opentensor/feat/thewhaleking/improve-scal…
thewhaleking Jan 29, 2025
7203256
Update bt-decode requirement
thewhaleking Jan 29, 2025
2f67746
Fixes decode-scale by setting the object to None correctly.
thewhaleking Jan 29, 2025
9d8086e
Merge pull request #19 from opentensor/feat/thewhaleking/fix-decode-s…
thewhaleking Jan 29, 2025
cf15184
Merge branch 'main' into backmerge-main-to-staging-rc5
ibraheem-abe Jan 29, 2025
bc0e928
Merge pull request #20 from opentensor/backmerge-main-to-staging-rc5
ibraheem-abe Jan 29, 2025
a4d9ba5
Bumps version and changelog
ibraheem-abe Jan 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.0.0rc7 /2025-01-29

## What's Changed
* feat: use bt_decode in runtime_call by @zyzniewski-reef in https://github.com/opentensor/async-substrate-interface/pull/15
* Move logic to mixin + fix tests by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/18
* Fix decode scale by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/19
* Backmerge main to staging rc5 by @ibraheem-opentensor in https://github.com/opentensor/async-substrate-interface/pull/20

## 1.0.0rc6 /2025-01-28

## What's Changed
Expand Down
174 changes: 86 additions & 88 deletions async_substrate_interface/async_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@

import asyncstdlib as a
from bittensor_wallet.keypair import Keypair
from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15
from bittensor_wallet.utils import SS58_FORMAT
from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject
from scalecodec.types import GenericCall, GenericRuntimeCallDefinition, GenericExtrinsic
from scalecodec.types import (
GenericCall,
GenericExtrinsic,
GenericRuntimeCallDefinition,
ss58_decode,
)
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed

Expand Down Expand Up @@ -789,8 +795,54 @@ async def load_registry(self):
)
metadata_option_hex_str = metadata_rpc_result["result"]
metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:])
metadata_v15 = MetadataV15.decode_from_metadata_option(metadata_option_bytes)
self.registry = PortableRegistry.from_metadata_v15(metadata_v15)
self.metadata_v15 = MetadataV15.decode_from_metadata_option(
metadata_option_bytes
)
self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15)

async def _wait_for_registry(self, _attempt: int = 1, _retries: int = 3) -> None:
async def _waiter():
while self.registry is None:
await asyncio.sleep(0.1)
return

try:
if not self.registry:
await asyncio.wait_for(_waiter(), timeout=10)
except TimeoutError:
# indicates that registry was never loaded
if not self._initializing:
raise AttributeError(
"Registry was never loaded. This did not occur during initialization, which usually indicates "
"you must first initialize the AsyncSubstrateInterface object, either with "
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
)
elif _attempt < _retries:
await self.load_registry()
return await self._wait_for_registry(_attempt + 1, _retries)
else:
raise AttributeError(
"Registry was never loaded. This occurred during initialization, which usually indicates a "
"connection or node error."
)

async def encode_scale(
self, type_string, value: Any, _attempt: int = 1, _retries: int = 3
) -> bytes:
"""
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string

Args:
type_string: the type string of the SCALE object for decoding
value: value to encode
_attempt: the current number of attempts to load the registry needed to encode the value
_retries: the maximum number of attempts to load the registry needed to encode the value

Returns:
encoded bytes
"""
await self._wait_for_registry(_attempt, _retries)
return self._encode_scale(type_string, value)

async def decode_scale(
self,
Expand All @@ -799,7 +851,7 @@ async def decode_scale(
_attempt=1,
_retries=3,
return_scale_obj=False,
) -> Any:
) -> Union[ScaleObj, Any]:
"""
Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string
(e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash
Expand All @@ -815,62 +867,20 @@ async def decode_scale(
Returns:
Decoded object
"""

async def _wait_for_registry():
while self.registry is None:
await asyncio.sleep(0.1)
return

if scale_bytes == b"\x00":
obj = None
else:
try:
if not self.registry:
await asyncio.wait_for(_wait_for_registry(), timeout=10)
if type_string == "scale_info::0": # Is an AccountId
# Decode AccountId bytes to SS58 address
return bytes.fromhex(ss58_decode(scale_bytes, SS58_FORMAT))
else:
await self._wait_for_registry(_attempt, _retries)
obj = decode_by_type_string(type_string, self.registry, scale_bytes)
except TimeoutError:
# indicates that registry was never loaded
if not self._initializing:
raise AttributeError(
"Registry was never loaded. This did not occur during initialization, which usually indicates "
"you must first initialize the AsyncSubstrateInterface object, either with "
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
)
elif _attempt < _retries:
await self.load_registry()
return await self.decode_scale(
type_string, scale_bytes, _attempt + 1
)
else:
raise AttributeError(
"Registry was never loaded. This occurred during initialization, which usually indicates a "
"connection or node error."
)
if return_scale_obj:
return ScaleObj(obj)
else:
return obj

async def encode_scale(self, type_string, value, block_hash=None) -> ScaleBytes:
"""
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string

Args:
type_string: the type string of the SCALE object for decoding
value: value to encode
block_hash: the hash of the blockchain block whose metadata to use for encoding

Returns:
ScaleBytes encoded value
"""
if not self._metadata or block_hash:
await self.init_runtime(block_hash=block_hash)

obj = self.runtime_config.create_scale_object(
type_string=type_string, metadata=self._metadata
)
return obj.encode(value)

async def _first_initialize_runtime(self):
"""
TODO docstring
Expand Down Expand Up @@ -2173,7 +2183,7 @@ async def query_multi(
await self.decode_scale(
storage_key.value_scale_type, change_data
),
)
),
)

return result
Expand Down Expand Up @@ -2503,56 +2513,43 @@ async def runtime_call(
params = {}

try:
runtime_call_def = self.runtime_config.type_registry["runtime_api"][api][
"methods"
][method]
runtime_api_types = self.runtime_config.type_registry["runtime_api"][
api
].get("types", {})
metadata_v15 = self.metadata_v15.value()
apis = {entry["name"]: entry for entry in metadata_v15["apis"]}
api_entry = apis[api]
methods = {entry["name"]: entry for entry in api_entry["methods"]}
runtime_call_def = methods[method]
except KeyError:
raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry")

if isinstance(params, list) and len(params) != len(runtime_call_def["params"]):
if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]):
raise ValueError(
f"Number of parameter provided ({len(params)}) does not "
f"match definition {len(runtime_call_def['params'])}"
f"match definition {len(runtime_call_def['inputs'])}"
)

# Add runtime API types to registry
self.runtime_config.update_type_registry_types(runtime_api_types)
runtime = Runtime(
self.chain,
self.runtime_config,
self._metadata,
self.type_registry,
)

# Encode params
param_data = ScaleBytes(bytes())
for idx, param in enumerate(runtime_call_def["params"]):
scale_obj = runtime.runtime_config.create_scale_object(param["type"])
param_data = b""
for idx, param in enumerate(runtime_call_def["inputs"]):
param_type_string = f"scale_info::{param['ty']}"
if isinstance(params, list):
param_data += scale_obj.encode(params[idx])
param_data += await self.encode_scale(param_type_string, params[idx])
else:
if param["name"] not in params:
raise ValueError(f"Runtime Call param '{param['name']}' is missing")

param_data += scale_obj.encode(params[param["name"]])
param_data += await self.encode_scale(
param_type_string, params[param["name"]]
)

# RPC request
result_data = await self.rpc_request(
"state_call", [f"{api}_{method}", str(param_data), block_hash]
"state_call", [f"{api}_{method}", param_data.hex(), block_hash]
)
output_type_string = f"scale_info::{runtime_call_def['output']}"

# Decode result
# TODO update this to use bt-decode
result_obj = runtime.runtime_config.create_scale_object(
runtime_call_def["type"]
)
result_obj.decode(
ScaleBytes(result_data["result"]),
check_remaining=self.config.get("strict_scale_decode"),
)
result_bytes = hex_to_bytes(result_data["result"])
result_obj = ScaleObj(await self.decode_scale(output_type_string, result_bytes))

return result_obj

Expand Down Expand Up @@ -2581,7 +2578,7 @@ async def get_account_next_index(self, account_address: str) -> int:
"""
This method maintains a cache of nonces for each account ss58address.
Upon subsequent calls, it will return the cached nonce + 1 instead of fetching from the chain.
This allows for correct nonce management in-case of async context when gathering co-routines.
This allows for correct nonce management in-case of async context when gathering co-routines.

Args:
account_address: SS58 formatted address
Expand All @@ -2595,7 +2592,9 @@ async def get_account_next_index(self, account_address: str) -> int:

async with self._lock:
if self._nonces.get(account_address) is None:
nonce_obj = await self.rpc_request("account_nextIndex", [account_address])
nonce_obj = await self.rpc_request(
"account_nextIndex", [account_address]
)
self._nonces[account_address] = nonce_obj["result"]
else:
self._nonces[account_address] += 1
Expand Down Expand Up @@ -2686,8 +2685,7 @@ async def get_payment_info(
extrinsic = await self.create_signed_extrinsic(
call=call, keypair=keypair, signature=signature
)
extrinsic_len = self.runtime_config.create_scale_object("u32")
extrinsic_len.encode(len(extrinsic.data))
extrinsic_len = len(extrinsic.data)

result = await self.runtime_call(
"TransactionPaymentApi", "query_info", [extrinsic, extrinsic_len]
Expand Down
Loading