Skip to content
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
24 changes: 17 additions & 7 deletions async_substrate_interface/async_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3467,30 +3467,40 @@ 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.
This allows for correct nonce management in-case of async context when gathering co-routines.

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]
Expand Down
51 changes: 51 additions & 0 deletions tests/unit_tests/asyncio_/test_substrate_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)