Skip to content

Commit 1f0325d

Browse files
authored
Merge pull request #272 from opentensor/release/1.6.2
Release/1.6.2
2 parents 36285e8 + 3696344 commit 1f0325d

15 files changed

Lines changed: 650 additions & 72 deletions

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ jobs:
124124
os:
125125
- ubuntu-latest
126126
test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }}
127-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
127+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
128128
steps:
129129
- name: Check-out repository
130130
uses: actions/checkout@v4

.github/workflows/unit-and-integration-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
fail-fast: false
1616
max-parallel: 5
1717
matrix:
18-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
18+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1919

2020
steps:
2121
- name: Checkout repository

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 1.6.2 /2025-02-19
4+
5+
## What's Changed
6+
* Typing by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/265
7+
* Cache Improvements by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/267
8+
* improve (async) query map result by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/266
9+
* Use threaded bt-decode by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/268
10+
* Feat: Handle new error message fmt for InBlock source by @ibraheem-abe in https://github.com/opentensor/async-substrate-interface/pull/269
11+
* Update: Remove python 3.9 support by @ibraheem-abe in https://github.com/opentensor/async-substrate-interface/pull/271
12+
13+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.6.1...v1.6.2
14+
315
## 1.6.1 /2025-02-03
416
* RuntimeCache updates by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/260
517
* fix memory leak by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/261

async_substrate_interface/async_substrate.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from scalecodec.types import (
3232
GenericCall,
3333
GenericExtrinsic,
34-
GenericRuntimeCallDefinition,
3534
ss58_encode,
3635
MultiAccountId,
3736
)
@@ -74,12 +73,10 @@
7473
_bt_decode_to_dict_or_list,
7574
legacy_scale_decode,
7675
convert_account_ids,
76+
decode_query_map_async,
7777
)
7878
from async_substrate_interface.utils.storage import StorageKey
7979
from async_substrate_interface.type_registry import _TYPE_REGISTRY
80-
from async_substrate_interface.utils.decoding import (
81-
decode_query_map,
82-
)
8380

8481
ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]]
8582

@@ -526,6 +523,18 @@ async def retrieve_next_page(self, start_key) -> list:
526523
self.last_key = result.last_key
527524
return result.records
528525

526+
async def retrieve_all_records(self) -> list[Any]:
527+
"""
528+
Retrieves all records from all subsequent pages for the AsyncQueryMapResult,
529+
returning them as a list.
530+
531+
Side effect:
532+
The self.records list will be populated fully after running this method.
533+
"""
534+
async for _ in self:
535+
pass
536+
return self.records
537+
529538
def __aiter__(self):
530539
return self
531540

@@ -558,6 +567,7 @@ async def __anext__(self):
558567
self.loading_complete = True
559568
raise StopAsyncIteration
560569

570+
self.records.extend(next_page)
561571
# Update the buffer with the newly fetched records
562572
self._buffer = iter(next_page)
563573
return next(self._buffer)
@@ -1408,7 +1418,9 @@ async def decode_scale(
14081418
if runtime is None:
14091419
runtime = await self.init_runtime(block_hash=block_hash)
14101420
if runtime.metadata_v15 is not None and force_legacy is False:
1411-
obj = decode_by_type_string(type_string, runtime.registry, scale_bytes)
1421+
obj = await asyncio.to_thread(
1422+
decode_by_type_string, type_string, runtime.registry, scale_bytes
1423+
)
14121424
if self.decode_ss58:
14131425
try:
14141426
type_str_int = int(type_string.split("::")[1])
@@ -2762,19 +2774,34 @@ async def rpc_request(
27622774
logger.error(f"Substrate Request Exception: {result[payload_id]}")
27632775
raise SubstrateRequestException(result[payload_id][0])
27642776

2765-
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
2766-
async def get_block_hash(self, block_id: int) -> str:
2777+
async def get_block_hash(self, block_id: Optional[int]) -> str:
27672778
"""
2768-
Retrieves the hash of the specified block number
2779+
Retrieves the hash of the specified block number, or the chaintip if None
27692780
Args:
27702781
block_id: block number
27712782
27722783
Returns:
27732784
Hash of the block
27742785
"""
2786+
if block_id is None:
2787+
return await self.get_chain_head()
2788+
else:
2789+
if (block_hash := self.runtime_cache.blocks.get(block_id)) is not None:
2790+
return block_hash
2791+
2792+
block_hash = await self._cached_get_block_hash(block_id)
2793+
self.runtime_cache.add_item(block_hash=block_hash, block=block_id)
2794+
return block_hash
2795+
2796+
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
2797+
async def _cached_get_block_hash(self, block_id: int) -> str:
2798+
"""
2799+
The design of this method is as such, because it allows for an easy drop-in for a different cache, such
2800+
as is the case with DiskCachedAsyncSubstrateInterface._cached_get_block_hash
2801+
"""
27752802
return await self._get_block_hash(block_id)
27762803

2777-
async def _get_block_hash(self, block_id: int) -> str:
2804+
async def _get_block_hash(self, block_id: Optional[int]) -> str:
27782805
return (await self.rpc_request("chain_getBlockHash", [block_id]))["result"]
27792806

27802807
async def get_chain_head(self) -> str:
@@ -3852,18 +3879,20 @@ async def query_map(
38523879
params=[result_keys, block_hash],
38533880
runtime=runtime,
38543881
)
3882+
changes = []
38553883
for result_group in response["result"]:
3856-
result = decode_query_map(
3857-
result_group["changes"],
3858-
prefix,
3859-
runtime,
3860-
param_types,
3861-
params,
3862-
value_type,
3863-
key_hashers,
3864-
ignore_decoding_errors,
3865-
self.decode_ss58,
3866-
)
3884+
changes.extend(result_group["changes"])
3885+
result = await decode_query_map_async(
3886+
changes,
3887+
prefix,
3888+
runtime,
3889+
param_types,
3890+
params,
3891+
value_type,
3892+
key_hashers,
3893+
ignore_decoding_errors,
3894+
self.decode_ss58,
3895+
)
38673896
else:
38683897
# storage item and value scale type are not included here because this is batch-decoded in rust
38693898
page_batches = [
@@ -3881,8 +3910,8 @@ async def query_map(
38813910
results: RequestResults = await self._make_rpc_request(
38823911
payloads, runtime=runtime
38833912
)
3884-
for result in results.values():
3885-
res = result[0]
3913+
for result_ in results.values():
3914+
res = result_[0]
38863915
if "error" in res:
38873916
err_msg = res["error"]["message"]
38883917
if (
@@ -3900,7 +3929,7 @@ async def query_map(
39003929
else:
39013930
for result_group in res["result"]:
39023931
changes.extend(result_group["changes"])
3903-
result = decode_query_map(
3932+
result = await decode_query_map_async(
39043933
changes,
39053934
prefix,
39063935
runtime,
@@ -4113,6 +4142,14 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
41134142
"extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()),
41144143
"finalized": False,
41154144
}, True
4145+
4146+
elif "params" in message and message["params"].get("result") == "invalid":
4147+
failure_message = f"Subscription {subscription_id} invalid: {message}"
4148+
async with self.ws as ws:
4149+
await ws.unsubscribe(subscription_id)
4150+
logger.error(failure_message)
4151+
raise SubstrateRequestException(failure_message)
4152+
41164153
return message, False
41174154

41184155
if wait_for_inclusion or wait_for_finalization:
@@ -4250,13 +4287,25 @@ async def get_metadata_event(
42504287

42514288
async def get_block_number(self, block_hash: Optional[str] = None) -> int:
42524289
"""Async version of `substrateinterface.base.get_block_number` method."""
4253-
response = await self.rpc_request("chain_getHeader", [block_hash])
4290+
if block_hash is None:
4291+
return await self._get_block_number(None)
4292+
if (block := self.runtime_cache.blocks_reverse.get(block_hash)) is not None:
4293+
return block
4294+
block = await self._cached_get_block_number(block_hash)
4295+
self.runtime_cache.add_item(block_hash=block_hash, block=block)
4296+
return block
42544297

4255-
if response["result"]:
4256-
return int(response["result"]["number"], 16)
4257-
raise SubstrateRequestException(
4258-
f"Unable to retrieve block number for {block_hash}"
4259-
)
4298+
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
4299+
async def _cached_get_block_number(self, block_hash: str) -> int:
4300+
"""
4301+
The design of this method is as such, because it allows for an easy drop-in for a different cache, such
4302+
as is the case with DiskCachedAsyncSubstrateInterface._cached_get_block_number
4303+
"""
4304+
return await self._get_block_number(block_hash=block_hash)
4305+
4306+
async def _get_block_number(self, block_hash: Optional[str]) -> int:
4307+
response = await self.rpc_request("chain_getHeader", [block_hash])
4308+
return int(response["result"]["number"], 16)
42604309

42614310
async def close(self):
42624311
"""
@@ -4351,9 +4400,13 @@ async def get_block_runtime_version_for(self, block_hash: str):
43514400
return await self._get_block_runtime_version_for(block_hash)
43524401

43534402
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
4354-
async def get_block_hash(self, block_id: int) -> str:
4403+
async def _cached_get_block_hash(self, block_id: int) -> str:
43554404
return await self._get_block_hash(block_id)
43564405

4406+
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
4407+
async def _cached_get_block_number(self, block_hash: str) -> int:
4408+
return await self._get_block_number(block_hash=block_hash)
4409+
43574410

43584411
async def get_async_substrate_interface(
43594412
url: str,

async_substrate_interface/py.typed

Whitespace-only changes.

async_substrate_interface/sync_substrate.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,18 @@ def retrieve_next_page(self, start_key) -> list:
482482
self.last_key = result.last_key
483483
return result.records
484484

485+
def retrieve_all_records(self) -> list[Any]:
486+
"""
487+
Retrieves all records from all subsequent pages for the QueryMapResult,
488+
returning them as a list.
489+
490+
Side effect:
491+
The self.records list will be populated fully after running this method.
492+
"""
493+
for _ in self:
494+
pass
495+
return self.records
496+
485497
def __iter__(self):
486498
return self
487499

@@ -511,6 +523,7 @@ def __next__(self):
511523
self.loading_complete = True
512524
raise StopIteration
513525

526+
self.records.extend(next_page)
514527
# Update the buffer with the newly fetched records
515528
self._buffer = iter(next_page)
516529
return next(self._buffer)
@@ -2052,8 +2065,21 @@ def rpc_request(
20522065
else:
20532066
raise SubstrateRequestException(result[payload_id][0])
20542067

2068+
def get_block_hash(self, block_id: Optional[int]) -> str:
2069+
"""
2070+
Retrieves the block hash for a given block number, or the chaintip hash if None
2071+
"""
2072+
if block_id is None:
2073+
return self.get_chain_head()
2074+
else:
2075+
if (block_hash := self.runtime_cache.blocks.get(block_id)) is not None:
2076+
return block_hash
2077+
block_hash = self._get_block_hash(block_id)
2078+
self.runtime_cache.add_item(block_hash=block_hash, block=block_id)
2079+
return block_hash
2080+
20552081
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
2056-
def get_block_hash(self, block_id: int) -> str:
2082+
def _get_block_hash(self, block_id: int) -> str:
20572083
return self.rpc_request("chain_getBlockHash", [block_id])["result"]
20582084

20592085
def get_chain_head(self) -> str:
@@ -3247,6 +3273,13 @@ def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
32473273
"extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()),
32483274
"finalized": False,
32493275
}, True
3276+
3277+
elif "params" in message and message["params"].get("result") == "invalid":
3278+
failure_message = f"Subscription {subscription_id} invalid: {message}"
3279+
self.rpc_request("author_unwatchExtrinsic", [subscription_id])
3280+
logger.error(failure_message)
3281+
raise SubstrateRequestException(failure_message)
3282+
32503283
return message, False
32513284

32523285
if wait_for_inclusion or wait_for_finalization:
@@ -3380,15 +3413,27 @@ def get_metadata_event(
33803413
return self._get_metadata_event(module_name, event_name, runtime)
33813414

33823415
def get_block_number(self, block_hash: Optional[str] = None) -> int:
3383-
"""Async version of `substrateinterface.base.get_block_number` method."""
3384-
response = self.rpc_request("chain_getHeader", [block_hash])
3385-
3386-
if response["result"]:
3387-
return int(response["result"]["number"], 16)
3416+
"""
3417+
Retrieves the block number for a given block hash or chaintip.
3418+
"""
3419+
if block_hash is None:
3420+
return self._get_block_number(None)
33883421
else:
3389-
raise SubstrateRequestException(
3390-
f"Unable to determine block number for {block_hash}"
3391-
)
3422+
if (
3423+
block_number := self.runtime_cache.blocks_reverse.get(block_hash)
3424+
) is not None:
3425+
return block_number
3426+
block_number = self._cached_get_block_number(block_hash=block_hash)
3427+
self.runtime_cache.add_item(block_hash=block_hash, block=block_number)
3428+
return block_number
3429+
3430+
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
3431+
def _cached_get_block_number(self, block_hash: Optional[str]) -> int:
3432+
return self._get_block_number(block_hash=block_hash)
3433+
3434+
def _get_block_number(self, block_hash: Optional[str]) -> int:
3435+
response = self.rpc_request("chain_getHeader", [block_hash])
3436+
return int(response["result"]["number"], 16)
33923437

33933438
def close(self):
33943439
"""
@@ -3404,6 +3449,7 @@ def close(self):
34043449
self.get_block_runtime_info.cache_clear()
34053450
self.get_block_runtime_version_for.cache_clear()
34063451
self.supports_rpc_method.cache_clear()
3407-
self.get_block_hash.cache_clear()
3452+
self._get_block_hash.cache_clear()
3453+
self._cached_get_block_number.cache_clear()
34083454

34093455
encode_scale = SubstrateMixin._encode_scale

0 commit comments

Comments
 (0)