Skip to content

Commit a7d1d3f

Browse files
authored
Merge pull request #3151 from opentensor/feat/roman/get_stake_info_for_coldkeys
[v10] Add `Async/Subtensor.get_stake_info_for_coldkeys` method
2 parents 71155a5 + 478dfde commit a7d1d3f

File tree

5 files changed

+296
-6
lines changed

5 files changed

+296
-6
lines changed

bittensor/core/async_subtensor.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3819,7 +3819,7 @@ async def get_stake_info_for_coldkey(
38193819
block: Optional[int] = None,
38203820
block_hash: Optional[str] = None,
38213821
reuse_block: bool = False,
3822-
) -> Optional[list["StakeInfo"]]:
3822+
) -> list["StakeInfo"]:
38233823
"""
38243824
Retrieves the stake information for a given coldkey.
38253825
@@ -3830,7 +3830,7 @@ async def get_stake_info_for_coldkey(
38303830
reuse_block: Whether to reuse the last-used block hash.
38313831
38323832
Returns:
3833-
An optional list of StakeInfo objects, or ``None`` if no stake information is found.
3833+
List of StakeInfo objects.
38343834
"""
38353835
result = await self.query_runtime_api(
38363836
runtime_api="StakeInfoRuntimeApi",
@@ -3847,6 +3847,42 @@ async def get_stake_info_for_coldkey(
38473847
stakes: list[StakeInfo] = StakeInfo.list_from_dicts(result)
38483848
return [stake for stake in stakes if stake.stake > 0]
38493849

3850+
async def get_stake_info_for_coldkeys(
3851+
self,
3852+
coldkey_ss58s: list[str],
3853+
block: Optional[int] = None,
3854+
block_hash: Optional[str] = None,
3855+
reuse_block: bool = False,
3856+
) -> dict[str, list["StakeInfo"]]:
3857+
"""
3858+
Retrieves the stake information for multiple coldkeys.
3859+
3860+
Parameters:
3861+
coldkey_ss58s: A list of SS58 addresses of the coldkeys to query.
3862+
block: The block number at which to query the stake information.
3863+
block_hash: The hash of the blockchain block number for the query.
3864+
reuse_block: Whether to reuse the last-used block hash.
3865+
3866+
Returns:
3867+
The dictionary mapping coldkey addresses to a list of StakeInfo objects.
3868+
"""
3869+
query = await self.query_runtime_api(
3870+
runtime_api="StakeInfoRuntimeApi",
3871+
method="get_stake_info_for_coldkeys",
3872+
params=[coldkey_ss58s],
3873+
block=block,
3874+
block_hash=block_hash,
3875+
reuse_block=reuse_block,
3876+
)
3877+
3878+
if query is None:
3879+
return {}
3880+
3881+
return {
3882+
decode_account_id(ck): StakeInfo.list_from_dicts(st_info)
3883+
for ck, st_info in query
3884+
}
3885+
38503886
async def get_stake_for_hotkey(
38513887
self,
38523888
hotkey_ss58: str,

bittensor/core/subtensor.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,7 +2905,7 @@ def get_stake_info_for_coldkey(
29052905
block: The block number at which to query the stake information.
29062906
29072907
Returns:
2908-
An optional list of StakeInfo objects, or ``None`` if no stake information is found.
2908+
List of StakeInfo objects.
29092909
"""
29102910
result = self.query_runtime_api(
29112911
runtime_api="StakeInfoRuntimeApi",
@@ -2916,8 +2916,35 @@ def get_stake_info_for_coldkey(
29162916

29172917
if result is None:
29182918
return []
2919-
stakes: list[StakeInfo] = StakeInfo.list_from_dicts(result)
2920-
return [stake for stake in stakes if stake.stake > 0]
2919+
return StakeInfo.list_from_dicts(result)
2920+
2921+
def get_stake_info_for_coldkeys(
2922+
self, coldkey_ss58s: list[str], block: Optional[int] = None
2923+
) -> dict[str, list["StakeInfo"]]:
2924+
"""
2925+
Retrieves the stake information for multiple coldkeys.
2926+
2927+
Parameters:
2928+
coldkey_ss58s: A list of SS58 addresses of the coldkeys to query.
2929+
block: The block number at which to query the stake information.
2930+
2931+
Returns:
2932+
The dictionary mapping coldkey addresses to a list of StakeInfo objects.
2933+
"""
2934+
query = self.query_runtime_api(
2935+
runtime_api="StakeInfoRuntimeApi",
2936+
method="get_stake_info_for_coldkeys",
2937+
params=[coldkey_ss58s],
2938+
block=block,
2939+
)
2940+
2941+
if query is None:
2942+
return {}
2943+
2944+
return {
2945+
decode_account_id(ck): StakeInfo.list_from_dicts(st_info)
2946+
for ck, st_info in query
2947+
}
29212948

29222949
def get_stake_for_hotkey(
29232950
self, hotkey_ss58: str, netuid: int, block: Optional[int] = None

bittensor/extras/subtensor_api/staking.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]):
2424
subtensor.get_stake_for_coldkey_and_hotkey
2525
)
2626
self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey
27+
self.get_stake_info_for_coldkeys = subtensor.get_stake_info_for_coldkeys
2728
self.get_stake_movement_fee = subtensor.get_stake_movement_fee
2829
self.get_stake_weight = subtensor.get_stake_weight
2930
self.get_unstake_fee = subtensor.get_unstake_fee

tests/unit_tests/test_async_subtensor.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5645,3 +5645,125 @@ async def test_blocks_until_next_epoch_uses_default_tempo(subtensor, mocker):
56455645
spy_tempo.assert_not_awaited()
56465646
assert result is not None
56475647
assert isinstance(result, int)
5648+
5649+
5650+
@pytest.mark.asyncio
5651+
async def test_get_stake_info_for_coldkeys_none(subtensor, mocker):
5652+
"""Tests get_stake_info_for_coldkeys method when query_runtime_api returns None."""
5653+
# Preps
5654+
fake_coldkey_ss58s = ["coldkey1", "coldkey2"]
5655+
fake_block = 123
5656+
fake_block_hash = None
5657+
fake_reuse_block = False
5658+
5659+
mocked_query_runtime_api = mocker.AsyncMock(
5660+
autospec=subtensor.query_runtime_api, return_value=None
5661+
)
5662+
subtensor.query_runtime_api = mocked_query_runtime_api
5663+
5664+
# Call
5665+
result = await subtensor.get_stake_info_for_coldkeys(
5666+
coldkey_ss58s=fake_coldkey_ss58s,
5667+
block=fake_block,
5668+
block_hash=fake_block_hash,
5669+
reuse_block=fake_reuse_block,
5670+
)
5671+
5672+
# Asserts
5673+
assert result == {}
5674+
mocked_query_runtime_api.assert_called_once_with(
5675+
runtime_api="StakeInfoRuntimeApi",
5676+
method="get_stake_info_for_coldkeys",
5677+
params=[fake_coldkey_ss58s],
5678+
block=fake_block,
5679+
block_hash=fake_block_hash,
5680+
reuse_block=fake_reuse_block,
5681+
)
5682+
5683+
5684+
@pytest.mark.asyncio
5685+
async def test_get_stake_info_for_coldkeys_success(subtensor, mocker):
5686+
"""Tests get_stake_info_for_coldkeys method when query_runtime_api returns data."""
5687+
# Preps
5688+
fake_coldkey_ss58s = ["coldkey1", "coldkey2"]
5689+
fake_block = 123
5690+
fake_block_hash = None
5691+
fake_reuse_block = False
5692+
5693+
fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c"
5694+
fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d"
5695+
fake_decoded_ck1 = "decoded_coldkey1"
5696+
fake_decoded_ck2 = "decoded_coldkey2"
5697+
5698+
stake_info_dict_1 = {
5699+
"netuid": 1,
5700+
"hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c",
5701+
"coldkey": fake_ck1,
5702+
"stake": 1000,
5703+
"locked": 0,
5704+
"emission": 100,
5705+
"drain": 0,
5706+
"is_registered": True,
5707+
}
5708+
stake_info_dict_2 = {
5709+
"netuid": 2,
5710+
"hotkey": b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d",
5711+
"coldkey": fake_ck2,
5712+
"stake": 2000,
5713+
"locked": 0,
5714+
"emission": 200,
5715+
"drain": 0,
5716+
"is_registered": False,
5717+
}
5718+
5719+
fake_query_result = [
5720+
(fake_ck1, [stake_info_dict_1]),
5721+
(fake_ck2, [stake_info_dict_2]),
5722+
]
5723+
5724+
mocked_query_runtime_api = mocker.AsyncMock(
5725+
autospec=subtensor.query_runtime_api, return_value=fake_query_result
5726+
)
5727+
subtensor.query_runtime_api = mocked_query_runtime_api
5728+
5729+
mocked_decode_account_id = mocker.patch.object(
5730+
async_subtensor,
5731+
"decode_account_id",
5732+
side_effect=[fake_decoded_ck1, fake_decoded_ck2],
5733+
)
5734+
5735+
mock_stake_info_1 = mocker.Mock(spec=StakeInfo)
5736+
mock_stake_info_2 = mocker.Mock(spec=StakeInfo)
5737+
mocked_stake_info_list_from_dicts = mocker.patch.object(
5738+
async_subtensor.StakeInfo,
5739+
"list_from_dicts",
5740+
side_effect=[[mock_stake_info_1], [mock_stake_info_2]],
5741+
)
5742+
5743+
# Call
5744+
result = await subtensor.get_stake_info_for_coldkeys(
5745+
coldkey_ss58s=fake_coldkey_ss58s,
5746+
block=fake_block,
5747+
block_hash=fake_block_hash,
5748+
reuse_block=fake_reuse_block,
5749+
)
5750+
5751+
# Asserts
5752+
assert result == {
5753+
fake_decoded_ck1: [mock_stake_info_1],
5754+
fake_decoded_ck2: [mock_stake_info_2],
5755+
}
5756+
mocked_query_runtime_api.assert_called_once_with(
5757+
runtime_api="StakeInfoRuntimeApi",
5758+
method="get_stake_info_for_coldkeys",
5759+
params=[fake_coldkey_ss58s],
5760+
block=fake_block,
5761+
block_hash=fake_block_hash,
5762+
reuse_block=fake_reuse_block,
5763+
)
5764+
mocked_decode_account_id.assert_has_calls(
5765+
[mocker.call(fake_ck1), mocker.call(fake_ck2)]
5766+
)
5767+
mocked_stake_info_list_from_dicts.assert_has_calls(
5768+
[mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])]
5769+
)

tests/unit_tests/test_subtensor.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4210,7 +4210,7 @@ def test_get_stake_weight(subtensor, mocker):
42104210
result = subtensor.get_stake_weight(netuid=netuid)
42114211

42124212
# Asserts
4213-
mock_determine_block_hash.assert_called_once_with(block=None)
4213+
mock_determine_block_hash.assert_called_once()
42144214
mocked_query.assert_called_once_with(
42154215
module="SubtensorModule",
42164216
storage_function="StakeWeight",
@@ -5762,3 +5762,107 @@ def test_blocks_until_next_epoch_uses_default_tempo(subtensor, mocker):
57625762
spy_tempo.assert_not_called()
57635763
assert result is not None
57645764
assert isinstance(result, int)
5765+
5766+
5767+
def test_get_stake_info_for_coldkeys_none(subtensor, mocker):
5768+
"""Tests get_stake_info_for_coldkeys method when query_runtime_api returns None."""
5769+
# Preps
5770+
fake_coldkey_ss58s = ["coldkey1", "coldkey2"]
5771+
fake_block = 123
5772+
5773+
mocked_query_runtime_api = mocker.patch.object(
5774+
subtensor, "query_runtime_api", return_value=None
5775+
)
5776+
5777+
# Call
5778+
result = subtensor.get_stake_info_for_coldkeys(
5779+
coldkey_ss58s=fake_coldkey_ss58s, block=fake_block
5780+
)
5781+
5782+
# Asserts
5783+
assert result == {}
5784+
mocked_query_runtime_api.assert_called_once_with(
5785+
runtime_api="StakeInfoRuntimeApi",
5786+
method="get_stake_info_for_coldkeys",
5787+
params=[fake_coldkey_ss58s],
5788+
block=fake_block,
5789+
)
5790+
5791+
5792+
def test_get_stake_info_for_coldkeys_success(subtensor, mocker):
5793+
"""Tests get_stake_info_for_coldkeys method when query_runtime_api returns data."""
5794+
# Preps
5795+
fake_coldkey_ss58s = ["coldkey1", "coldkey2"]
5796+
fake_block = 123
5797+
5798+
fake_ck1 = b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c"
5799+
fake_ck2 = b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d"
5800+
fake_decoded_ck1 = "decoded_coldkey1"
5801+
fake_decoded_ck2 = "decoded_coldkey2"
5802+
5803+
stake_info_dict_1 = {
5804+
"netuid": 5,
5805+
"hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c",
5806+
"coldkey": fake_ck1,
5807+
"stake": 1000,
5808+
"locked": 0,
5809+
"emission": 100,
5810+
"drain": 0,
5811+
"is_registered": True,
5812+
}
5813+
stake_info_dict_2 = {
5814+
"netuid": 14,
5815+
"hotkey": b"\x17:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1d",
5816+
"coldkey": fake_ck2,
5817+
"stake": 2000,
5818+
"locked": 0,
5819+
"emission": 200,
5820+
"drain": 0,
5821+
"is_registered": False,
5822+
}
5823+
5824+
fake_query_result = [
5825+
(fake_ck1, [stake_info_dict_1]),
5826+
(fake_ck2, [stake_info_dict_2]),
5827+
]
5828+
5829+
mocked_query_runtime_api = mocker.patch.object(
5830+
subtensor, "query_runtime_api", return_value=fake_query_result
5831+
)
5832+
5833+
mocked_decode_account_id = mocker.patch.object(
5834+
subtensor_module,
5835+
"decode_account_id",
5836+
side_effect=[fake_decoded_ck1, fake_decoded_ck2],
5837+
)
5838+
5839+
mock_stake_info_1 = mocker.Mock(spec=StakeInfo)
5840+
mock_stake_info_2 = mocker.Mock(spec=StakeInfo)
5841+
mocked_stake_info_list_from_dicts = mocker.patch.object(
5842+
subtensor_module.StakeInfo,
5843+
"list_from_dicts",
5844+
side_effect=[[mock_stake_info_1], [mock_stake_info_2]],
5845+
)
5846+
5847+
# Call
5848+
result = subtensor.get_stake_info_for_coldkeys(
5849+
coldkey_ss58s=fake_coldkey_ss58s, block=fake_block
5850+
)
5851+
5852+
# Asserts
5853+
assert result == {
5854+
fake_decoded_ck1: [mock_stake_info_1],
5855+
fake_decoded_ck2: [mock_stake_info_2],
5856+
}
5857+
mocked_query_runtime_api.assert_called_once_with(
5858+
runtime_api="StakeInfoRuntimeApi",
5859+
method="get_stake_info_for_coldkeys",
5860+
params=[fake_coldkey_ss58s],
5861+
block=fake_block,
5862+
)
5863+
mocked_decode_account_id.assert_has_calls(
5864+
[mocker.call(fake_ck1), mocker.call(fake_ck2)]
5865+
)
5866+
mocked_stake_info_list_from_dicts.assert_has_calls(
5867+
[mocker.call([stake_info_dict_1]), mocker.call([stake_info_dict_2])]
5868+
)

0 commit comments

Comments
 (0)