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
110 changes: 101 additions & 9 deletions bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,13 +646,21 @@ async def all_subnets(
)
if not block_hash and reuse_block:
block_hash = self.substrate.last_block_hash
query = await self.substrate.runtime_call(
"SubnetInfoRuntimeApi",
"get_all_dynamic_info",
block_hash=block_hash,

query, subnet_prices = await asyncio.gather(
self.substrate.runtime_call(
"SubnetInfoRuntimeApi",
"get_all_dynamic_info",
block_hash=block_hash,
),
self.get_subnet_prices(),
)
subnets = DynamicInfo.list_from_dicts(query.decode())
return subnets

decoded = query.decode()

for sn in decoded:
sn.update({"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))})
return DynamicInfo.list_from_dicts(decoded)

async def blocks_since_last_step(
self,
Expand Down Expand Up @@ -902,8 +910,13 @@ async def get_all_subnets_info(
)
if not result:
return []
else:
return SubnetInfo.list_from_dicts(result)

subnets_prices = await self.get_subnet_prices()

for subnet in result:
subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)})

return SubnetInfo.list_from_dicts(result)

async def get_balance(
self,
Expand Down Expand Up @@ -2267,6 +2280,84 @@ async def get_subnet_info(
return None
return SubnetInfo.from_dict(result)

async def get_subnet_price(
self,
netuid: int,
block: Optional[int] = None,
block_hash: Optional[str] = None,
reuse_block: bool = False,
) -> Balance:
"""Gets the current Alpha price in TAO for all subnets.

Arguments:
netuid: The unique identifier of the subnet.
block: The blockchain block number for the query.
block_hash (Optional[str]): The hash of the block to retrieve the stake from. Do not specify if using block
or reuse_block
reuse_block (bool): Whether to use the last-used block. Do not set if using block_hash or block.

Returns:
The current Alpha price in TAO units for the specified subnet.
"""
# SN0 price is always 1 TAO
if netuid == 0:
return Balance.from_tao(1)

block_hash = await self.determine_block_hash(
block=block, block_hash=block_hash, reuse_block=reuse_block
)
current_sqrt_price = await self.substrate.query(
module="Swap",
storage_function="AlphaSqrtPrice",
params=[netuid],
block_hash=block_hash,
)

current_sqrt_price = fixed_to_float(current_sqrt_price)
current_price = current_sqrt_price * current_sqrt_price
return Balance.from_rao(int(current_price * 1e9))

async def get_subnet_prices(
self,
block: Optional[int] = None,
block_hash: Optional[str] = None,
reuse_block: bool = False,
) -> dict[int, Balance]:
"""Gets the current Alpha price in TAO for a specified subnet.

Args:
block: The blockchain block number for the query.
block_hash (Optional[str]): The hash of the block to retrieve the stake from. Do not specify if using block
or reuse_block
reuse_block (bool): Whether to use the last-used block. Do not set if using block_hash or block.

Returns:
dict:
- subnet unique ID
- The current Alpha price in TAO units for the specified subnet.
"""
block_hash = await self.determine_block_hash(
block=block, block_hash=block_hash, reuse_block=reuse_block
)

current_sqrt_prices = await self.substrate.query_map(
module="Swap",
storage_function="AlphaSqrtPrice",
block_hash=block_hash,
page_size=129, # total number of subnets
)

prices = {}
async for id_, current_sqrt_price in current_sqrt_prices:
current_sqrt_price = fixed_to_float(current_sqrt_price)
current_price = current_sqrt_price * current_sqrt_price
current_price_in_tao = Balance.from_rao(int(current_price * 1e9))
prices.update({id_: current_price_in_tao})

# SN0 price is always 1 TAO
prices.update({0: Balance.from_tao(1)})
return prices

async def get_unstake_fee(
self,
amount: Balance,
Expand Down Expand Up @@ -3336,7 +3427,8 @@ async def subnet(
)

if isinstance(decoded := query.decode(), dict):
return DynamicInfo.from_dict(decoded)
price = self.get_subnet_price(netuid=netuid, block=block)
return DynamicInfo.from_dict({**decoded, "price": price})
return None

async def subnet_exists(
Expand Down
13 changes: 5 additions & 8 deletions bittensor/core/chain_data/dynamic_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DynamicInfo(InfoBase):
alpha_in: Balance
alpha_out: Balance
tao_in: Balance
price: Balance
price: Optional[Balance]
k: float
is_dynamic: bool
alpha_out_emission: Balance
Expand Down Expand Up @@ -74,13 +74,6 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo":
).set_unit(0)

subnet_volume = Balance.from_rao(decoded["subnet_volume"]).set_unit(netuid)
price = (
Balance.from_tao(1.0)
if netuid == 0
else Balance.from_tao(tao_in.tao / alpha_in.tao).set_unit(netuid)
if alpha_in.tao > 0
else Balance.from_tao(1).set_unit(netuid)
) # Root always has 1-1 price

if decoded.get("subnet_identity"):
subnet_identity = SubnetIdentity(
Expand All @@ -97,6 +90,10 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo":
)
else:
subnet_identity = None
price = decoded.get("price", None)

if price and not isinstance(price, Balance):
raise ValueError(f"price must be a Balance object, got {type(price)}.")

return cls(
netuid=netuid,
Expand Down
83 changes: 79 additions & 4 deletions bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,12 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo
"get_all_dynamic_info",
block_hash=block_hash,
)
return DynamicInfo.list_from_dicts(query.decode())
subnet_prices = self.get_subnet_prices()
decoded = query.decode()

for sn in decoded:
sn.update({"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))})
return DynamicInfo.list_from_dicts(decoded)

def blocks_since_last_step(
self, netuid: int, block: Optional[int] = None
Expand Down Expand Up @@ -636,8 +641,13 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"
)
if not result:
return []
else:
return SubnetInfo.list_from_dicts(result)

subnets_prices = self.get_subnet_prices()

for subnet in result:
subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)})

return SubnetInfo.list_from_dicts(result)

def get_balance(self, address: str, block: Optional[int] = None) -> Balance:
"""
Expand Down Expand Up @@ -1813,6 +1823,70 @@ def get_subnet_info(
return None
return SubnetInfo.from_dict(result)

def get_subnet_price(
self,
netuid: int,
block: Optional[int] = None,
) -> Balance:
"""Gets the current Alpha price in TAO for all subnets.

Arguments:
netuid: The unique identifier of the subnet.
block: The blockchain block number for the query.

Returns:
The current Alpha price in TAO units for the specified subnet.
"""
# SN0 price is always 1 TAO
if netuid == 0:
return Balance.from_tao(1)

block_hash = self.determine_block_hash(block=block)
current_sqrt_price = self.substrate.query(
module="Swap",
storage_function="AlphaSqrtPrice",
params=[netuid],
block_hash=block_hash,
)

current_sqrt_price = fixed_to_float(current_sqrt_price)
current_price = current_sqrt_price * current_sqrt_price
return Balance.from_rao(int(current_price * 1e9))

def get_subnet_prices(
self,
block: Optional[int] = None,
) -> dict[int, Balance]:
"""Gets the current Alpha price in TAO for a specified subnet.

Args:
block: The blockchain block number for the query. Default to `None`.

Returns:
dict:
- subnet unique ID
- The current Alpha price in TAO units for the specified subnet.
"""
block_hash = self.determine_block_hash(block=block)

current_sqrt_prices = self.substrate.query_map(
module="Swap",
storage_function="AlphaSqrtPrice",
block_hash=block_hash,
page_size=129, # total number of subnets
)

prices = {}
for id_, current_sqrt_price in current_sqrt_prices:
current_sqrt_price = fixed_to_float(current_sqrt_price)
current_price = current_sqrt_price * current_sqrt_price
current_price_in_tao = Balance.from_rao(int(current_price * 1e9))
prices.update({id_: current_price_in_tao})

# SN0 price is always 1 TAO
prices.update({0: Balance.from_tao(1)})
return prices

def get_unstake_fee(
self,
amount: Balance,
Expand Down Expand Up @@ -2636,7 +2710,8 @@ def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicIn
)

if isinstance(decoded := query.decode(), dict):
return DynamicInfo.from_dict(decoded)
price = self.get_subnet_price(netuid=netuid, block=block)
return DynamicInfo.from_dict({**decoded, "price": price})
return None

def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions bittensor/core/subtensor_api/subnets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]):
self.get_subnet_burn_cost = subtensor.get_subnet_burn_cost
self.get_subnet_hyperparameters = subtensor.get_subnet_hyperparameters
self.get_subnet_info = subtensor.get_subnet_info
self.get_subnet_price = subtensor.get_subnet_price
self.get_subnet_prices = subtensor.get_subnet_prices
self.get_subnet_owner_hotkey = subtensor.get_subnet_owner_hotkey
self.get_subnet_reveal_period_epochs = subtensor.get_subnet_reveal_period_epochs
self.get_subnet_validator_permits = subtensor.get_subnet_validator_permits
Expand Down
2 changes: 2 additions & 0 deletions bittensor/core/subtensor_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def add_legacy_methods(subtensor: "SubtensorApi"):
subtensor._subtensor.get_subnet_hyperparameters
)
subtensor.get_subnet_info = subtensor._subtensor.get_subnet_info
subtensor.get_subnet_price = subtensor._subtensor.get_subnet_price
subtensor.get_subnet_prices = subtensor._subtensor.get_subnet_prices
subtensor.get_subnet_owner_hotkey = subtensor._subtensor.get_subnet_owner_hotkey
subtensor.get_subnet_reveal_period_epochs = (
subtensor._subtensor.get_subnet_reveal_period_epochs
Expand Down
19 changes: 10 additions & 9 deletions tests/integration_tests/test_subtensor_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ async def prepare_test(mocker, seed, **subtensor_args):
return subtensor


@pytest.mark.asyncio
async def test_get_all_subnets_info(mocker):
subtensor = await prepare_test(mocker, "get_all_subnets_info")
result = subtensor.get_all_subnets_info()
assert isinstance(result, list)
assert result[0].owner_ss58 == "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"
assert result[1].kappa == 32767
assert result[1].max_weight_limit == 65535
assert result[1].blocks_since_epoch == 88
# TODO: Improve integration tests workflow (https://github.com/opentensor/bittensor/issues/2435#issuecomment-2825858004)
# @pytest.mark.asyncio
# async def test_get_all_subnets_info(mocker):
# subtensor = await prepare_test(mocker, "get_all_subnets_info")
# result = subtensor.get_all_subnets_info()
# assert isinstance(result, list)
# assert result[0].owner_ss58 == "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"
# assert result[1].kappa == 32767
# assert result[1].max_weight_limit == 65535
# assert result[1].blocks_since_epoch == 88


# TODO: Improve integration tests workflow (https://github.com/opentensor/bittensor/issues/2435#issuecomment-2825858004)
Expand Down
62 changes: 62 additions & 0 deletions tests/unit_tests/test_async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3813,3 +3813,65 @@ async def test_toggle_user_liquidity(subtensor, fake_wallet, mocker):
period=None,
)
assert result == mocked_extrinsic.return_value


@pytest.mark.asyncio
async def test_get_subnet_price(subtensor, mocker):
"""Test get_subnet_price returns the correct value."""
# preps
netuid = 123
mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash")
fake_price = {"bits": 3155343338053956962}
expected_price = Balance.from_tao(0.029258617)
mocked_query = mocker.patch.object(
subtensor.substrate, "query", return_value=fake_price
)

# Call
result = await subtensor.get_subnet_price(
netuid=netuid,
)

# Asserts
mocked_determine_block_hash.assert_awaited_once_with(
block=None, block_hash=None, reuse_block=False
)
mocked_query.assert_awaited_once_with(
module="Swap",
storage_function="AlphaSqrtPrice",
params=[netuid],
block_hash=mocked_determine_block_hash.return_value,
)

assert result == expected_price


@pytest.mark.asyncio
async def test_get_subnet_prices(subtensor, mocker):
"""Test get_subnet_prices returns the correct value."""
# preps
mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash")

async def fake_current_sqrt_prices():
yield [0, {"bits": 0}]
yield [1, {"bits": 3155343338053956962}]

expected_prices = {0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)}
mocked_query_map = mocker.patch.object(
subtensor.substrate, "query_map", return_value=fake_current_sqrt_prices()
)

# Call
result = await subtensor.get_subnet_prices()

# Asserts
mocked_determine_block_hash.assert_awaited_once_with(
block=None, block_hash=None, reuse_block=False
)
mocked_query_map.assert_awaited_once_with(
module="Swap",
storage_function="AlphaSqrtPrice",
block_hash=mocked_determine_block_hash.return_value,
page_size=129, # total number of subnets
)
assert result == expected_prices
Loading