diff --git a/async_substrate_interface/async_substrate.py b/async_substrate_interface/async_substrate.py index dcc5ceb..90ac49d 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, use_cache: bool = True + ) -> 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 + 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 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] diff --git a/tests/unit_tests/asyncio_/test_substrate_interface.py b/tests/unit_tests/asyncio_/test_substrate_interface.py index 632af81..afefe7a 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, + use_cache=False, + ) + + 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", + use_cache=False, + )