diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md index 8bf781b532..e3c29ef96d 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -56,4 +56,8 @@ Examples: - Fixed an issue where multiple cursors did not work in a file with a single line. - Increased the performance of searching and replacing across a whole project. ---> \ No newline at end of file +--> + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_change.md b/.github/PULL_REQUEST_TEMPLATE/feature_change.md index 0b29a822b3..15292a9d7a 100644 --- a/.github/PULL_REQUEST_TEMPLATE/feature_change.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature_change.md @@ -51,4 +51,8 @@ Examples: - Fixed an issue where multiple cursors did not work in a file with a single line. - Increased the performance of searching and replacing across a whole project. ---> \ No newline at end of file +--> + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md index 96e18c9d29..0419f9f88d 100644 --- a/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md +++ b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md @@ -52,4 +52,8 @@ Examples: - Fixed an issue where multiple cursors did not work in a file with a single line. - Increased the performance of searching and replacing across a whole project. ---> \ No newline at end of file +--> + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4a5da46aee..a58c86f015 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,6 +5,6 @@ please switch to **Preview** for links to render properly. Please choose the right template for your pull request: -- 🐛 Are you fixing a bug? [Bug fix](?template=bug_fix.md) -- 📈 Are you improving performance? [Performance improvement](?template=performance_improvement.md) -- 💻 Are you changing functionality? [Feature change](?template=feature_change.md) +- 🐛 Are you fixing a bug? [Bug fix](?expand=1&template=bug_fix.md) +- 📈 Are you improving performance? [Performance improvement](?expand=1&template=performance_improvement.md) +- 💻 Are you changing functionality? [Feature change](?expand=1&template=feature_change.md) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 494da0b011..364b96698b 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -128,11 +128,31 @@ jobs: fi done + # run non-fast-blocks only on Saturday and by cron schedule + check-if-saturday: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + outputs: + is-saturday: ${{ steps.check.outputs.is-saturday }} + steps: + - id: check + run: | + day=$(date -u +%u) + echo "Today is weekday $day" + if [ "$day" -ne 6 ]; then + echo "⏭️ Skipping: not Saturday" + echo "is-saturday=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "is-saturday=true" + echo "is-saturday=true" >> "$GITHUB_OUTPUT" + cron-run-non-fast-blocks-e2e-test: - if: github.event_name == 'schedule' + if: github.event_name == 'schedule' && needs.check-if-saturday.outputs.is-saturday == 'true' name: "NFB: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" needs: + - check-if-saturday - find-tests - pull-docker-image runs-on: ubuntu-latest @@ -148,14 +168,6 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - name: Check if today is Saturday - run: | - day=$(date -u +%u) - echo "Today is weekday $day" - if [ "$day" -ne 6 ]; then - echo "⏭️ Skipping: not Saturday" - exit 78 - fi - name: Check-out repository uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5190ea2525..80bf2ad1b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 9.7.0 /2025-05-29 + +## What's Changed +* Add `get_subnet_info` by @basfroman in https://github.com/opentensor/bittensor/pull/2894 +* Fix bug in `get_next_epoch_start_block` by @basfroman in https://github.com/opentensor/bittensor/pull/2899 +* e2e workflow: improve skipping logic (no error when skip the job) by @basfroman in https://github.com/opentensor/bittensor/pull/2898 +* Replace `transfer_allow_death` with `transfer_keep_alive` by @basfroman in https://github.com/opentensor/bittensor/pull/2900 +* Fix broken pull request template links (#2867) by @AgSpades in https://github.com/opentensor/bittensor/pull/2883 +* update pr templates with branch ack by @thewhaleking in https://github.com/opentensor/bittensor/pull/2903 + +## New Contributors +* @AgSpades made their first contribution in https://github.com/opentensor/bittensor/pull/2883 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.6.1...v9.7.0 + ## 9.6.1 /2025-05-22 ## What's Changed diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index aa5fa9c5f1..da7d75a82f 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1796,7 +1796,7 @@ async def get_next_epoch_start_block( netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block ) - if block and blocks_since_last_step and tempo: + if block and blocks_since_last_step is not None and tempo: return block - blocks_since_last_step + tempo + 1 return None @@ -1921,6 +1921,42 @@ async def get_stake_add_fee( ) return Balance.from_rao(result) + async def get_subnet_info( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["SubnetInfo"]: + """ + Retrieves detailed information about subnet within the Bittensor network. + This function provides comprehensive data on subnet, including its characteristics and operational parameters. + + 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: + SubnetInfo: A SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnet's details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if not result: + return None + return SubnetInfo.from_dict(result) + async def get_unstake_fee( self, amount: Balance, @@ -2286,7 +2322,7 @@ async def get_transfer_fee( call = await self.substrate.compose_call( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": dest, "value": value.rao}, ) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index a1c781310c..e98e234307 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -45,7 +45,7 @@ async def _do_transfer( """ call = await subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": destination, "value": amount.rao}, ) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 03624097d0..a2ac8df11d 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -44,7 +44,7 @@ def _do_transfer( """ call = subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": destination, "value": amount.rao}, ) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 015b918cd7..406295e330 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1385,7 +1385,7 @@ def get_next_epoch_start_block( blocks_since_last_step = self.blocks_since_last_step(netuid=netuid, block=block) tempo = self.tempo(netuid=netuid, block=block) - if block and blocks_since_last_step and tempo: + if block and blocks_since_last_step is not None and tempo: return block - blocks_since_last_step + tempo + 1 return None @@ -1507,6 +1507,33 @@ def get_stake_add_fee( ) return Balance.from_rao(result) + def get_subnet_info( + self, netuid: int, block: Optional[int] = None + ) -> Optional["SubnetInfo"]: + """ + Retrieves detailed information about subnet within the Bittensor network. + This function provides comprehensive data on subnet, including its characteristics and operational parameters. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + SubnetInfo: A SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnet's details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + if not result: + return None + return SubnetInfo.from_dict(result) + def get_unstake_fee( self, amount: Balance, @@ -1804,7 +1831,7 @@ def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balan value = check_and_convert_to_balance(value) call = self.substrate.compose_call( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": dest, "value": value.rao}, ) diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py index 8b8c9121e7..962f761d1e 100644 --- a/bittensor/core/subtensor_api/subnets.py +++ b/bittensor/core/subtensor_api/subnets.py @@ -24,6 +24,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_next_epoch_start_block = subtensor.get_next_epoch_start_block 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_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 diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index 3f8cc7d36d..0ca9b1234a 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -84,6 +84,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_subnet_hyperparameters = ( subtensor._subtensor.get_subnet_hyperparameters ) + subtensor.get_subnet_info = subtensor._subtensor.get_subnet_info subtensor.get_subnet_owner_hotkey = subtensor._subtensor.get_subnet_owner_hotkey subtensor.get_subnet_reveal_period_epochs = ( subtensor._subtensor.get_subnet_reveal_period_epochs diff --git a/pyproject.toml b/pyproject.toml index 244fcd3a53..15a1062fbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.6.1" +version = "9.7.0" description = "Bittensor" readme = "README.md" authors = [ diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 77fcd72f9f..95c5249b62 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -32,7 +32,7 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) @@ -77,7 +77,7 @@ async def test_do_transfer_failure(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) @@ -124,7 +124,7 @@ async def test_do_transfer_no_waiting(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index fcd532e2d0..081a56ffae 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -26,7 +26,7 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) subtensor.sign_and_send_extrinsic.assert_called_once_with( @@ -64,7 +64,7 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) subtensor.sign_and_send_extrinsic.assert_called_once_with( @@ -103,7 +103,7 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) subtensor.sign_and_send_extrinsic.assert_called_once_with( diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 0449a7b8ef..a023be1c62 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -707,7 +707,7 @@ async def test_get_transfer_fee(subtensor, fake_wallet, mocker, balance): mocked_compose_call.assert_awaited_once() mocked_compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={ "dest": fake_dest, "value": fake_value.rao, @@ -3314,3 +3314,94 @@ async def test_get_subnet_validator_permits_is_none(subtensor, mocker): ) assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_info_success(mocker, subtensor): + """Test get_subnet_info returns correct data when subnet information is found.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + async_subtensor.SubnetInfo, + "from_dict", + ) + + # Call + result = await subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_awaited_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=None, + reuse_block=False, + ) + async_subtensor.SubnetInfo.from_dict.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + assert result == async_subtensor.SubnetInfo.from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_subnet_info_no_data(mocker, subtensor): + """Test get_subnet_info returns None.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + mocker.patch.object(async_subtensor.SubnetInfo, "from_dict") + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + + # Call + result = await subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_awaited_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=None, + reuse_block=False, + ) + async_subtensor.SubnetInfo.from_dict.assert_not_called() + assert result is None + + +@pytest.mark.parametrize( + "call_return, expected", + [[10, 111], [None, None], [0, 121]], +) +@pytest.mark.asyncio +async def test_get_next_epoch_start_block(mocker, subtensor, call_return, expected): + """Check that get_next_epoch_start_block returns the correct value.""" + # Prep + netuid = mocker.Mock() + block = 20 + + fake_block_hash = mocker.Mock() + mocker.patch.object(subtensor, "get_block_hash", return_value=fake_block_hash) + + mocked_blocks_since_last_step = mocker.AsyncMock(return_value=call_return) + subtensor.blocks_since_last_step = mocked_blocks_since_last_step + + mocker.patch.object(subtensor, "tempo", return_value=100) + + # Call + result = await subtensor.get_next_epoch_start_block(netuid=netuid, block=block) + + # Asserts + mocked_blocks_since_last_step.assert_called_once_with( + netuid=netuid, + block=block, + block_hash=fake_block_hash, + reuse_block=False, + ) + subtensor.tempo.assert_awaited_once_with( + netuid=netuid, block=block, block_hash=fake_block_hash, reuse_block=False + ) + assert result == expected diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0db7c0cafe..2499db8248 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1877,7 +1877,7 @@ def test_get_transfer_fee(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_allow_death", + call_function="transfer_keep_alive", call_params={"dest": fake_dest, "value": value.rao}, ) @@ -3668,3 +3668,81 @@ def test_is_subnet_active(subtensor, mocker, query_return, expected): ) assert result == expected + + +# `geg_l_subnet_info` tests +def test_get_subnet_info_success(mocker, subtensor): + """Test get_subnet_info returns correct data when subnet information is found.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + subtensor_module.SubnetInfo, + "from_dict", + ) + + # Call + result = subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetInfo.from_dict.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + assert result == subtensor_module.SubnetInfo.from_dict.return_value + + +def test_get_subnet_info_no_data(mocker, subtensor): + """Test get_subnet_info returns None.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + mocker.patch.object(subtensor_module.SubnetInfo, "from_dict") + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + + # Call + result = subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetInfo.from_dict.assert_not_called() + assert result is None + + +@pytest.mark.parametrize( + "call_return, expected", + [[10, 111], [None, None], [0, 121]], +) +def test_get_next_epoch_start_block(mocker, subtensor, call_return, expected): + """Check that get_next_epoch_start_block returns the correct value.""" + # Prep + netuid = mocker.Mock() + block = 20 + + mocked_blocks_since_last_step = mocker.Mock(return_value=call_return) + subtensor.blocks_since_last_step = mocked_blocks_since_last_step + + mocker.patch.object(subtensor, "tempo", return_value=100) + + # Call + result = subtensor.get_next_epoch_start_block(netuid=netuid, block=block) + + # Asserts + mocked_blocks_since_last_step.assert_called_once_with( + netuid=netuid, + block=block, + ) + subtensor.tempo.assert_called_once_with(netuid=netuid, block=block) + assert result == expected