From cc7356b5493113d983c65eed40a6f972296f77e4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 23 Feb 2026 21:51:35 -0800 Subject: [PATCH 1/6] extend async `get_account_next_index` logic --- async_substrate_interface/async_substrate.py | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index dcc5ceb..3bc91c6 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -3467,7 +3467,9 @@ async def get_account_nonce(self, account_address: str) -> int: ) return response["nonce"] - async def get_account_next_index(self, account_address: str) -> int: + async def get_account_next_index( + self, account_address: str, do_not_use_cache: bool = False + ) -> 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. @@ -3475,22 +3477,30 @@ async def get_account_next_index(self, account_address: str) -> int: Args: account_address: SS58 formatted address + do_not_use_cache: If True, bypass local nonce cache and always request fresh value from RPC. Returns: Next index for the given account address """ + + async def _get_account_next_index(): + """Inner RPC call to get `account_nextIndex`.""" + nonce_obj_ = await self.rpc_request("account_nextIndex", [account_address]) + if "error" in nonce_obj_: + raise SubstrateRequestException(nonce_obj_["error"]["message"]) + return nonce_obj_["result"] + if not await self.supports_rpc_method("account_nextIndex"): # Unlikely to happen, this is a common RPC method raise Exception("account_nextIndex not supported") + if do_not_use_cache: + return await _get_account_next_index() + async with self._lock: if self._nonces.get(account_address) is None: - nonce_obj = await self.rpc_request( - "account_nextIndex", [account_address] - ) - if "error" in nonce_obj: - raise SubstrateRequestException(nonce_obj["error"]["message"]) - self._nonces[account_address] = nonce_obj["result"] + nonce_obj = await _get_account_next_index() + self._nonces[account_address] = nonce_obj else: self._nonces[account_address] += 1 return self._nonces[account_address] From b07fa296f7b0aec14a17f5299e84d20ef785ddf0 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 23 Feb 2026 21:51:43 -0800 Subject: [PATCH 2/6] add unit tests --- .../asyncio_/test_substrate_interface.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unit_tests/asyncio_/test_substrate_interface.py b/tests/unit_tests/asyncio_/test_substrate_interface.py index 632af81..562b1ca 100644 --- a/tests/unit_tests/asyncio_/test_substrate_interface.py +++ b/tests/unit_tests/asyncio_/test_substrate_interface.py @@ -11,6 +11,7 @@ AsyncSubstrateInterface, get_async_substrate_interface, ) +from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.types import ScaleObj from tests.helpers.settings import ARCHIVE_ENTRYPOINT, LATENT_LITE_ENTRYPOINT @@ -287,3 +288,53 @@ async def test_cache_miss_fetches_and_stores(self, substrate): substrate.runtime_cache.add_item.assert_called_once_with( block_hash="0xABC", block=100 ) + + +@pytest.mark.asyncio +async def test_get_account_next_index_cached_mode_uses_internal_cache(): + substrate = AsyncSubstrateInterface("ws://localhost", _mock=True) + substrate.supports_rpc_method = AsyncMock(return_value=True) + substrate.rpc_request = AsyncMock(return_value={"result": 5}) + + first = await substrate.get_account_next_index("5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA") + second = await substrate.get_account_next_index( + "5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA" + ) + + assert first == 5 + assert second == 6 + substrate.rpc_request.assert_awaited_once_with( + "account_nextIndex", ["5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA"] + ) + + +@pytest.mark.asyncio +async def test_get_account_next_index_bypass_mode_does_not_create_or_mutate_cache(): + substrate = AsyncSubstrateInterface("ws://localhost", _mock=True) + substrate.supports_rpc_method = AsyncMock(return_value=True) + substrate.rpc_request = AsyncMock(return_value={"result": 10}) + + address = "5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA" + assert address not in substrate._nonces + + result = await substrate.get_account_next_index( + address, + do_not_use_cache=True, + ) + + assert result == 10 + assert address not in substrate._nonces + substrate.rpc_request.assert_awaited_once_with("account_nextIndex", [address]) + + +@pytest.mark.asyncio +async def test_get_account_next_index_bypass_mode_raises_on_rpc_error(): + substrate = AsyncSubstrateInterface("ws://localhost", _mock=True) + substrate.supports_rpc_method = AsyncMock(return_value=True) + substrate.rpc_request = AsyncMock(return_value={"error": {"message": "rpc failure"}}) + + with pytest.raises(SubstrateRequestException, match="rpc failure"): + await substrate.get_account_next_index( + "5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA", + do_not_use_cache=True, + ) From 43eb352a4eb414b9105af8d88bf98fcf6a32be6d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 08:06:14 -0800 Subject: [PATCH 3/6] trigger commit From 87d6182585029315489695edf98491483bebe194 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 24 Feb 2026 10:20:57 -0800 Subject: [PATCH 4/6] rename the parameter --- async_substrate_interface/async_substrate.py | 6 +++--- tests/unit_tests/asyncio_/test_substrate_interface.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index 3bc91c6..90ac49d 100644 --- a/async_substrate_interface/async_substrate.py +++ b/async_substrate_interface/async_substrate.py @@ -3468,7 +3468,7 @@ async def get_account_nonce(self, account_address: str) -> int: return response["nonce"] async def get_account_next_index( - self, account_address: str, do_not_use_cache: bool = False + self, account_address: str, use_cache: bool = True ) -> int: """ This method maintains a cache of nonces for each account ss58address. @@ -3477,7 +3477,7 @@ async def get_account_next_index( Args: account_address: SS58 formatted address - do_not_use_cache: If True, bypass local nonce cache and always request fresh value from RPC. + use_cache: If True, bypass local nonce cache and always request fresh value from RPC. Returns: Next index for the given account address @@ -3494,7 +3494,7 @@ async def _get_account_next_index(): # Unlikely to happen, this is a common RPC method raise Exception("account_nextIndex not supported") - if do_not_use_cache: + if not use_cache: return await _get_account_next_index() async with self._lock: diff --git a/tests/unit_tests/asyncio_/test_substrate_interface.py b/tests/unit_tests/asyncio_/test_substrate_interface.py index 562b1ca..afefe7a 100644 --- a/tests/unit_tests/asyncio_/test_substrate_interface.py +++ b/tests/unit_tests/asyncio_/test_substrate_interface.py @@ -319,7 +319,7 @@ async def test_get_account_next_index_bypass_mode_does_not_create_or_mutate_cach result = await substrate.get_account_next_index( address, - do_not_use_cache=True, + use_cache=False, ) assert result == 10 @@ -336,5 +336,5 @@ async def test_get_account_next_index_bypass_mode_raises_on_rpc_error(): with pytest.raises(SubstrateRequestException, match="rpc failure"): await substrate.get_account_next_index( "5F3sa2TJAWMqDhXG6jhV4N8ko9NoFz5Y2s8vS8uM9f7v7mA", - do_not_use_cache=True, + use_cache=False, ) From ee89a9eecea6c0a0a850cd1c1f4aa63ac19fd8d4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 11:33:37 -0800 Subject: [PATCH 5/6] testing --- async_substrate_interface/sync_substrate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/async_substrate_interface/sync_substrate.py b/async_substrate_interface/sync_substrate.py index f6ff5b6..d393c25 100644 --- a/async_substrate_interface/sync_substrate.py +++ b/async_substrate_interface/sync_substrate.py @@ -764,9 +764,12 @@ def decode_scale( return ss58_encode(scale_bytes, self.ss58_format) else: if self.runtime.metadata_v15 is not None and force_legacy is False: - obj = decode_by_type_string( - type_string, self.runtime.registry, scale_bytes - ) + try: + obj = decode_by_type_string( + type_string, self.runtime.registry, scale_bytes + ) + except ValueError: + obj = legacy_scale_decode(type_string, scale_bytes, self.runtime) if self.decode_ss58: try: type_str_int = int(type_string.split("::")[1]) From 10314284b1f4f50bbfd4b1144d6e91fedfc3ed51 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 13:30:34 -0800 Subject: [PATCH 6/6] bump version and changelog --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c601550..019fccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.6.3 /2025-02-24 + +## What's Changed +* Fix: Supp for third-party runtimes by @ibraheem-abe in https://github.com/opentensor/async-substrate-interface/pull/274 +* Extend `get_account_next_index` logic by @basfroman in https://github.com/opentensor/async-substrate-interface/pull/273 + +**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.6.2...v1.6.3 + ## 1.6.2 /2025-02-19 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index c7e6eef..a00d36b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "async-substrate-interface" -version = "1.6.2" +version = "1.6.3" description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface" readme = "README.md" license = { file = "LICENSE" }