Skip to content

feat: use bt_decode in runtime_call #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 79 additions & 47 deletions async_substrate_interface/async_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@

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,
encode as encode_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 @@ -788,8 +799,10 @@ 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 decode_scale(
self,
Expand Down Expand Up @@ -822,6 +835,9 @@ async def _wait_for_registry():

if scale_bytes == b"\x00":
obj = None
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:
try:
if not self.registry:
Expand Down Expand Up @@ -850,25 +866,55 @@ async def _wait_for_registry():
else:
return obj

async def encode_scale(self, type_string, value, block_hash=None) -> ScaleBytes:
async def encode_scale(self, type_string, value: Any) -> 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
block_hash: the hash of the blockchain block whose metadata to use for encoding

Returns:
ScaleBytes encoded value
encoded SCALE bytes
"""
if not self._metadata or block_hash:
await self.init_runtime(block_hash=block_hash)
if value is None:
result = b"\x00"
else:
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<AccountId>
if not isinstance(value, (list, tuple)):
value = [value]

# Encode length
length = len(value)
if length < 64:
result = bytes([length << 2]) # Single byte mode
else:
raise ValueError("Vector length too large")

obj = self.runtime_config.create_scale_object(
type_string=type_string, metadata=self._metadata
)
return obj.encode(value)
# Encode each AccountId
for account in value:
if isinstance(account, bytes):
result += account # Already encoded
else:
result += bytes.fromhex(
ss58_decode(value, SS58_FORMAT)
) # SS58 string
return result

if isinstance(value, ScaleType):
if value.data.data is not None:
# Already encoded
return bytes(value.data.data)
else:
value = value.value # Unwrap the value of the type

result = bytes(encode_by_type_string(type_string, self.registry, value))
return result

async def _first_initialize_runtime(self):
"""
Expand Down Expand Up @@ -2172,7 +2218,7 @@ async def query_multi(
await self.decode_scale(
storage_key.value_scale_type, change_data
),
)
),
)

return result
Expand Down Expand Up @@ -2502,56 +2548,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 @@ -2678,8 +2711,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