From 510f72826fe08d3133522ca47b5e5cde1726bb64 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Sat, 20 Dec 2025 08:21:31 +0100 Subject: [PATCH 01/40] feat: centralized retry and backoff for SDK network operations --- bittensor/core/dendrite.py | 55 +++++++++++++--- bittensor/core/subtensor.py | 84 +++++++++++++++++++++---- bittensor/utils/retry.py | 111 ++++++++++++++++++++++++++++++++ tests/unit_tests/test_retry.py | 112 +++++++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 22 deletions(-) create mode 100644 bittensor/utils/retry.py create mode 100644 tests/unit_tests/test_retry.py diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index e559a1a026..3382b6cef2 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -15,6 +15,7 @@ from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.utils import networking +from bittensor.utils.retry import retry_async from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch @@ -575,16 +576,29 @@ async def call( self._log_outgoing_request(synapse) # Make the HTTP POST request - async with (await self.session).post( - url=url, - headers=synapse.to_headers(), - json=synapse.model_dump(), - timeout=aiohttp.ClientTimeout(total=timeout), - ) as response: - # Extract the JSON response from the server - json_response = await response.json() - # Process the server response and fill synapse - self.process_server_response(response, json_response, synapse) + async def _make_request(): + async with (await self.session).post( + url=url, + headers=synapse.to_headers(), + json=synapse.model_dump(), + timeout=aiohttp.ClientTimeout(total=timeout), + ) as response: + # Extract the JSON response from the server + json_response = await response.json() + # Process the server response and fill synapse + self.process_server_response(response, json_response, synapse) + + # Retry the request if enabled + await retry_async( + _make_request, + retry_exceptions=( + aiohttp.ClientConnectorError, + asyncio.TimeoutError, + aiohttp.ServerDisconnectedError, + aiohttp.ServerConnectionError, + aiohttp.ClientError, + ), + ) # Set process time and log the response synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore @@ -650,6 +664,27 @@ async def call_stream( self._log_outgoing_request(synapse) # Make the HTTP POST request + async def _make_stream_request(): + async with (await self.session).post( + url, + headers=synapse.to_headers(), + json=synapse.model_dump(), + timeout=aiohttp.ClientTimeout(total=timeout), + ) as response: + # Use synapse subclass' process_streaming_response method to yield the response chunks + async for chunk in synapse.process_streaming_response(response): # type: ignore + yield chunk # Yield each chunk as it's processed + json_response = synapse.extract_response_json(response) + + # Process the server response + self.process_server_response(response, json_response, synapse) + + # NOTE: streaming requests (`call_stream`) are intentionally NOT retried here. + # Async generators cannot be safely retried once they start yielding data without buffering + # or complex replay logic, which risks protocol side effects. + # To enable retries for streaming, the higher-level caller must handle the restart logic. + + async with (await self.session).post( url, headers=synapse.to_headers(), diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 804d160bce..be8896aa2e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -163,6 +163,7 @@ price_to_tick, tick_to_price, ) +from bittensor.utils.retry import retry_call if TYPE_CHECKING: from async_substrate_interface.sync_substrate import QueryMapResult @@ -563,14 +564,20 @@ def get_hyperparameter( logging.error(f"subnet {netuid} does not exist") return None - result = self.substrate.query( + return retry_call( + self.substrate.query, module="SubtensorModule", storage_function=param_name, params=[netuid], block_hash=block_hash, - ) - - return getattr(result, "value", result) + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), + ).value @property def block(self) -> int: @@ -704,10 +711,18 @@ def query_constant( Common types include int (for counts/blocks), Balance objects (for amounts in Rao), and booleans. """ - return self.substrate.get_constant( + return retry_call( + self.substrate.get_constant, module_name=module_name, constant_name=constant_name, block_hash=self.determine_block_hash(block), + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), ) def query_map( @@ -731,11 +746,19 @@ def query_map( Returns: QueryMapResult: A data structure representing the map storage if found, None otherwise. """ - result = self.substrate.query_map( + result = retry_call( + self.substrate.query_map, module=module, storage_function=name, params=params, block_hash=self.determine_block_hash(block=block), + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), ) return result @@ -758,11 +781,19 @@ def query_map_subtensor( Returns: An object containing the map-like data structure, or `None` if not found. """ - return self.substrate.query_map( + return retry_call( + self.substrate.query_map, module="SubtensorModule", storage_function=name, params=params, block_hash=self.determine_block_hash(block), + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), ) def query_module( @@ -787,13 +818,22 @@ def query_module( An object containing the requested data if found, `None` otherwise. """ - return self.substrate.query( + return retry_call( + self.substrate.query, module=module, storage_function=name, params=params, block_hash=self.determine_block_hash(block), + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), ) + def query_runtime_api( self, runtime_api: str, @@ -816,9 +856,21 @@ def query_runtime_api( """ block_hash = self.determine_block_hash(block) - result = self.substrate.runtime_call(runtime_api, method, params, block_hash) - - return result.value + + return retry_call( + self.substrate.runtime_call, + api=runtime_api, + method=method, + params=params, + block_hash=block_hash, + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), + ).value def query_subtensor( self, @@ -839,11 +891,19 @@ def query_subtensor( Returns: query_response: An object containing the requested data. """ - return self.substrate.query( + return retry_call( + self.substrate.query, module="SubtensorModule", storage_function=name, params=params, block_hash=self.determine_block_hash(block), + retry_exceptions=( + SubstrateRequestException, + ConnectionRefusedError, + TimeoutError, + BrokenPipeError, + ConnectionResetError, + ), ) def state_call( diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py new file mode 100644 index 0000000000..876fd8bb1b --- /dev/null +++ b/bittensor/utils/retry.py @@ -0,0 +1,111 @@ +import asyncio +import os +import time +import random +import logging +from typing import Type, Tuple, Optional, Callable, Any, Union + +logger = logging.getLogger(__name__) + +# Helpers for runtime environment variable access +def _retry_enabled() -> bool: + return os.environ.get("BT_RETRY_ENABLED", "False").lower() in ("true", "1", "yes", "on") + +def _retry_max_attempts() -> int: + return int(os.environ.get("BT_RETRY_MAX_ATTEMPTS", 3)) + +def _retry_base_delay() -> float: + return float(os.environ.get("BT_RETRY_BASE_DELAY", 1.0)) + +def _retry_max_delay() -> float: + return float(os.environ.get("BT_RETRY_MAX_DELAY", 60.0)) + +_RETRY_BACKOFF_FACTOR = 2.0 + +def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> float: + """Calculates backoff time with exponential backoff and jitter.""" + delay = min(max_delay, base_delay * (_RETRY_BACKOFF_FACTOR ** attempt)) + # Add jitter: random value between 0 and delay + return delay * (0.5 + random.random()) + +def retry_call( + func: Callable, + *args, + retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (OSError, TimeoutError), + max_attempts: Optional[int] = None, + base_delay: Optional[float] = None, + max_delay: Optional[float] = None, + **kwargs +) -> Any: + """ + Synchronous retry wrapper. + + If BT_RETRY_ENABLED is False, executes the function exactly once. + """ + if not _retry_enabled(): + return func(*args, **kwargs) + + # Resolve configuration + _max_attempts = max_attempts if max_attempts is not None else _retry_max_attempts() + _base_delay = base_delay if base_delay is not None else _retry_base_delay() + _max_delay = max_delay if max_delay is not None else _retry_max_delay() + + last_exception = None + + for attempt in range(1, _max_attempts + 1): + try: + return func(*args, **kwargs) + except retry_exceptions as e: + last_exception = e + if attempt == _max_attempts: + logger.debug(f"Retry exhausted after {_max_attempts} attempts. Last error: {e}") + raise e + + backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) + logger.debug(f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s...") + time.sleep(backoff) + + if last_exception: + raise last_exception + return None # Should not be reached + +async def retry_async( + func: Callable, + *args, + retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (OSError, TimeoutError), + max_attempts: Optional[int] = None, + base_delay: Optional[float] = None, + max_delay: Optional[float] = None, + **kwargs +) -> Any: + """ + Asynchronous retry wrapper. + + If BT_RETRY_ENABLED is False, executes the function exactly once. + """ + if not _retry_enabled(): + return await func(*args, **kwargs) + + # Resolve configuration + _max_attempts = max_attempts if max_attempts is not None else _retry_max_attempts() + _base_delay = base_delay if base_delay is not None else _retry_base_delay() + _max_delay = max_delay if max_delay is not None else _retry_max_delay() + + last_exception = None + + for attempt in range(1, _max_attempts + 1): + try: + return await func(*args, **kwargs) + except retry_exceptions as e: + last_exception = e + if attempt == _max_attempts: + logger.debug(f"Retry exhausted after {_max_attempts} attempts. Last error: {e}") + raise e + + backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) + logger.debug(f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s...") + await asyncio.sleep(backoff) + + if last_exception: + raise last_exception + return None # Should not be reached diff --git a/tests/unit_tests/test_retry.py b/tests/unit_tests/test_retry.py new file mode 100644 index 0000000000..861ab45cca --- /dev/null +++ b/tests/unit_tests/test_retry.py @@ -0,0 +1,112 @@ +import pytest +import time +import asyncio +from unittest.mock import Mock, patch, AsyncMock +from bittensor.utils.retry import retry_call, retry_async + +# Create custom exception for testing +class NetworkError(Exception): + pass + +class NonRetryableError(Exception): + pass + +@pytest.fixture +def mock_sleep(): + with patch("time.sleep") as m: + yield m + +@pytest.fixture +def mock_async_sleep(): + with patch("asyncio.sleep", new_callable=AsyncMock) as m: + yield m + +@pytest.fixture +def enable_retries(): + # Patch environment variables + with patch.dict("os.environ", {"BT_RETRY_ENABLED": "True"}): + yield + +@pytest.fixture +def disable_retries(): + # Patch environment variables + with patch.dict("os.environ", {"BT_RETRY_ENABLED": "False"}): + yield + +# --- Sync Tests --- + +def test_sync_retry_success(enable_retries): + mock_func = Mock(return_value="success") + result = retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert result == "success" + assert mock_func.call_count == 1 + +def test_sync_retry_eventual_success(enable_retries, mock_sleep): + mock_func = Mock(side_effect=[NetworkError("Fail 1"), NetworkError("Fail 2"), "success"]) + result = retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert result == "success" + assert mock_func.call_count == 3 + +def test_sync_retry_exhaustion(enable_retries, mock_sleep): + mock_func = Mock(side_effect=NetworkError("Persistent Fail")) + with pytest.raises(NetworkError, match="Persistent Fail"): + retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 3 + +def test_sync_no_retry_on_wrong_exception(enable_retries): + mock_func = Mock(side_effect=NonRetryableError("Fatal")) + with pytest.raises(NonRetryableError, match="Fatal"): + retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 1 + +def test_sync_disabled_retries_executes_once(disable_retries): + mock_func = Mock(side_effect=NetworkError("Fail")) + with pytest.raises(NetworkError, match="Fail"): + retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 1 + +def test_sync_default_retry_exceptions_do_not_retry_non_network_error(enable_retries): + mock_func = Mock(side_effect=ValueError("bad input")) + with pytest.raises(ValueError, match="bad input"): + # Should raise immediately because ValueError is not in (OSError, TimeoutError) + retry_call(mock_func) + assert mock_func.call_count == 1 + + +# --- Async Tests --- + +@pytest.mark.asyncio +async def test_async_retry_success(enable_retries): + mock_func = AsyncMock(return_value="success") + result = await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert result == "success" + assert mock_func.call_count == 1 + +@pytest.mark.asyncio +async def test_async_retry_eventual_success(enable_retries, mock_async_sleep): + mock_func = AsyncMock(side_effect=[NetworkError("Fail 1"), NetworkError("Fail 2"), "success"]) + result = await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert result == "success" + assert mock_func.call_count == 3 + +@pytest.mark.asyncio +async def test_async_retry_exhaustion(enable_retries, mock_async_sleep): + mock_func = AsyncMock(side_effect=NetworkError("Persistent Fail")) + with pytest.raises(NetworkError, match="Persistent Fail"): + await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 3 + +@pytest.mark.asyncio +async def test_async_no_retry_on_wrong_exception(enable_retries): + mock_func = AsyncMock(side_effect=NonRetryableError("Fatal")) + with pytest.raises(NonRetryableError, match="Fatal"): + await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 1 + +@pytest.mark.asyncio +async def test_async_disabled_retries_executes_once(disable_retries): + mock_func = AsyncMock(side_effect=NetworkError("Fail")) + with pytest.raises(NetworkError, match="Fail"): + await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + assert mock_func.call_count == 1 + From 3575822515ecf6b76ec6eec92dbb1fdfda814525 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Sat, 20 Dec 2025 08:50:40 +0100 Subject: [PATCH 02/40] chore: format retry utility to satisfy ruff --- bittensor/utils/retry.py | 71 +++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 876fd8bb1b..3d8d291877 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -7,39 +7,54 @@ logger = logging.getLogger(__name__) + # Helpers for runtime environment variable access def _retry_enabled() -> bool: - return os.environ.get("BT_RETRY_ENABLED", "False").lower() in ("true", "1", "yes", "on") + return os.environ.get("BT_RETRY_ENABLED", "False").lower() in ( + "true", + "1", + "yes", + "on", + ) + def _retry_max_attempts() -> int: return int(os.environ.get("BT_RETRY_MAX_ATTEMPTS", 3)) + def _retry_base_delay() -> float: return float(os.environ.get("BT_RETRY_BASE_DELAY", 1.0)) + def _retry_max_delay() -> float: return float(os.environ.get("BT_RETRY_MAX_DELAY", 60.0)) + _RETRY_BACKOFF_FACTOR = 2.0 + def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> float: """Calculates backoff time with exponential backoff and jitter.""" - delay = min(max_delay, base_delay * (_RETRY_BACKOFF_FACTOR ** attempt)) + delay = min(max_delay, base_delay * (_RETRY_BACKOFF_FACTOR**attempt)) # Add jitter: random value between 0 and delay return delay * (0.5 + random.random()) + def retry_call( func: Callable, *args, - retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (OSError, TimeoutError), + retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = ( + OSError, + TimeoutError, + ), max_attempts: Optional[int] = None, base_delay: Optional[float] = None, max_delay: Optional[float] = None, - **kwargs + **kwargs, ) -> Any: """ Synchronous retry wrapper. - + If BT_RETRY_ENABLED is False, executes the function exactly once. """ if not _retry_enabled(): @@ -49,38 +64,46 @@ def retry_call( _max_attempts = max_attempts if max_attempts is not None else _retry_max_attempts() _base_delay = base_delay if base_delay is not None else _retry_base_delay() _max_delay = max_delay if max_delay is not None else _retry_max_delay() - + last_exception = None - + for attempt in range(1, _max_attempts + 1): try: return func(*args, **kwargs) except retry_exceptions as e: last_exception = e if attempt == _max_attempts: - logger.debug(f"Retry exhausted after {_max_attempts} attempts. Last error: {e}") + logger.debug( + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + ) raise e - + backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) - logger.debug(f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s...") + logger.debug( + f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." + ) time.sleep(backoff) - + if last_exception: raise last_exception - return None # Should not be reached + return None # Should not be reached + async def retry_async( func: Callable, *args, - retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (OSError, TimeoutError), + retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = ( + OSError, + TimeoutError, + ), max_attempts: Optional[int] = None, base_delay: Optional[float] = None, max_delay: Optional[float] = None, - **kwargs + **kwargs, ) -> Any: """ Asynchronous retry wrapper. - + If BT_RETRY_ENABLED is False, executes the function exactly once. """ if not _retry_enabled(): @@ -90,22 +113,26 @@ async def retry_async( _max_attempts = max_attempts if max_attempts is not None else _retry_max_attempts() _base_delay = base_delay if base_delay is not None else _retry_base_delay() _max_delay = max_delay if max_delay is not None else _retry_max_delay() - + last_exception = None - + for attempt in range(1, _max_attempts + 1): try: return await func(*args, **kwargs) except retry_exceptions as e: last_exception = e if attempt == _max_attempts: - logger.debug(f"Retry exhausted after {_max_attempts} attempts. Last error: {e}") + logger.debug( + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + ) raise e - + backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) - logger.debug(f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s...") + logger.debug( + f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." + ) await asyncio.sleep(backoff) - + if last_exception: raise last_exception - return None # Should not be reached + return None # Should not be reached From 18b2576db13ad3ba7e28a7b2f7d5a27c235e75c1 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Sat, 20 Dec 2025 08:51:35 +0100 Subject: [PATCH 03/40] fix(subtensor): guard against None query results in get_hyperparameter --- bittensor/core/subtensor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index be8896aa2e..58d9b10b83 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -564,7 +564,7 @@ def get_hyperparameter( logging.error(f"subnet {netuid} does not exist") return None - return retry_call( + result = retry_call( self.substrate.query, module="SubtensorModule", storage_function=param_name, @@ -577,7 +577,15 @@ def get_hyperparameter( BrokenPipeError, ConnectionResetError, ), - ).value + ) + + if result is None: + return None + + if hasattr(result, "value"): + return result.value + + return result @property def block(self) -> int: From 94d58942ccead4796bcd8bc1315f498a0fe2138f Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Sat, 20 Dec 2025 08:51:39 +0100 Subject: [PATCH 04/40] ci: fix e2e subtensor label detection without gh auth --- .github/workflows/e2e-subtensor-tests.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index b83308ad12..cd603b5111 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -127,8 +127,7 @@ jobs: echo "Reading labels ..." if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - # Use GitHub CLI to read labels (works for forks too) - labels=$(gh pr view ${{ github.event.pull_request.number }} -R ${{ github.repository }} --json labels --jq '.labels[].name' || echo "") + labels=$(jq -r '.pull_request.labels[]?.name' "$GITHUB_EVENT_PATH" | tr '\n' ' ' || echo "") echo "Found labels: $labels" else labels="" From 5e3c65f3f2f01c30ca625293a7d9d06ea7cf2227 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Sun, 21 Dec 2025 22:13:20 +0100 Subject: [PATCH 05/40] chore: remove default retry usage and keep retry utility optional --- bittensor/core/dendrite.py | 41 ++++++-------------- bittensor/core/subtensor.py | 75 +++++-------------------------------- bittensor/utils/retry.py | 4 ++ 3 files changed, 24 insertions(+), 96 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 3382b6cef2..a3891ecbe1 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -15,7 +15,7 @@ from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.utils import networking -from bittensor.utils.retry import retry_async + from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch @@ -576,29 +576,16 @@ async def call( self._log_outgoing_request(synapse) # Make the HTTP POST request - async def _make_request(): - async with (await self.session).post( - url=url, - headers=synapse.to_headers(), - json=synapse.model_dump(), - timeout=aiohttp.ClientTimeout(total=timeout), - ) as response: - # Extract the JSON response from the server - json_response = await response.json() - # Process the server response and fill synapse - self.process_server_response(response, json_response, synapse) - - # Retry the request if enabled - await retry_async( - _make_request, - retry_exceptions=( - aiohttp.ClientConnectorError, - asyncio.TimeoutError, - aiohttp.ServerDisconnectedError, - aiohttp.ServerConnectionError, - aiohttp.ClientError, - ), - ) + async with (await self.session).post( + url=url, + headers=synapse.to_headers(), + json=synapse.model_dump(), + timeout=aiohttp.ClientTimeout(total=timeout), + ) as response: + # Extract the JSON response from the server + json_response = await response.json() + # Process the server response and fill synapse + self.process_server_response(response, json_response, synapse) # Set process time and log the response synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore @@ -678,13 +665,7 @@ async def _make_stream_request(): # Process the server response self.process_server_response(response, json_response, synapse) - - # NOTE: streaming requests (`call_stream`) are intentionally NOT retried here. - # Async generators cannot be safely retried once they start yielding data without buffering - # or complex replay logic, which risks protocol side effects. - # To enable retries for streaming, the higher-level caller must handle the restart logic. - async with (await self.session).post( url, headers=synapse.to_headers(), diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 58d9b10b83..8665e79327 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -163,7 +163,7 @@ price_to_tick, tick_to_price, ) -from bittensor.utils.retry import retry_call + if TYPE_CHECKING: from async_substrate_interface.sync_substrate import QueryMapResult @@ -564,19 +564,11 @@ def get_hyperparameter( logging.error(f"subnet {netuid} does not exist") return None - result = retry_call( - self.substrate.query, + result = self.substrate.query( module="SubtensorModule", storage_function=param_name, params=[netuid], block_hash=block_hash, - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) if result is None: @@ -719,18 +711,10 @@ def query_constant( Common types include int (for counts/blocks), Balance objects (for amounts in Rao), and booleans. """ - return retry_call( - self.substrate.get_constant, + return self.substrate.get_constant( module_name=module_name, constant_name=constant_name, block_hash=self.determine_block_hash(block), - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) def query_map( @@ -754,19 +738,11 @@ def query_map( Returns: QueryMapResult: A data structure representing the map storage if found, None otherwise. """ - result = retry_call( - self.substrate.query_map, + result = self.substrate.query_map( module=module, storage_function=name, params=params, block_hash=self.determine_block_hash(block=block), - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) return result @@ -789,19 +765,11 @@ def query_map_subtensor( Returns: An object containing the map-like data structure, or `None` if not found. """ - return retry_call( - self.substrate.query_map, + return self.substrate.query_map( module="SubtensorModule", storage_function=name, params=params, block_hash=self.determine_block_hash(block), - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) def query_module( @@ -826,22 +794,13 @@ def query_module( An object containing the requested data if found, `None` otherwise. """ - return retry_call( - self.substrate.query, + return self.substrate.query( module=module, storage_function=name, params=params, block_hash=self.determine_block_hash(block), - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) - def query_runtime_api( self, runtime_api: str, @@ -864,20 +823,12 @@ def query_runtime_api( """ block_hash = self.determine_block_hash(block) - - return retry_call( - self.substrate.runtime_call, + + return self.substrate.runtime_call( api=runtime_api, method=method, params=params, block_hash=block_hash, - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ).value def query_subtensor( @@ -899,19 +850,11 @@ def query_subtensor( Returns: query_response: An object containing the requested data. """ - return retry_call( - self.substrate.query, + return self.substrate.query( module="SubtensorModule", storage_function=name, params=params, block_hash=self.determine_block_hash(block), - retry_exceptions=( - SubstrateRequestException, - ConnectionRefusedError, - TimeoutError, - BrokenPipeError, - ConnectionResetError, - ), ) def state_call( diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 3d8d291877..c2602a1e1b 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -7,6 +7,10 @@ logger = logging.getLogger(__name__) +# Note: This utility is not used internally by the SDK. +# It is provided as an optional helper for users who wish +# to implement consistent retry behavior themselves. + # Helpers for runtime environment variable access def _retry_enabled() -> bool: From ef12886696bee8775a78c3fa3c467ad9e22719cd Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:00:54 -0800 Subject: [PATCH 06/40] Move test_retry.py to utils directory --- tests/unit_tests/{ => utils}/test_retry.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit_tests/{ => utils}/test_retry.py (100%) diff --git a/tests/unit_tests/test_retry.py b/tests/unit_tests/utils/test_retry.py similarity index 100% rename from tests/unit_tests/test_retry.py rename to tests/unit_tests/utils/test_retry.py From 2085e2de6ae68d0b9be828382977fbf750a41685 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:05:23 -0800 Subject: [PATCH 07/40] Change logger name to 'bittensor.utils.retry' --- bittensor/utils/retry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index c2602a1e1b..ade454263e 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -5,7 +5,7 @@ import logging from typing import Type, Tuple, Optional, Callable, Any, Union -logger = logging.getLogger(__name__) +logger = logging.getLogger("bittensor.utils.retry") # Note: This utility is not used internally by the SDK. # It is provided as an optional helper for users who wish From 5b7ae44a1bb981e0ba50f88538b4b81d226a6a45 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:09:04 -0800 Subject: [PATCH 08/40] Refactor HTTP request handling in dendrite.py Removed the internal function for making HTTP POST requests and processing responses. --- bittensor/core/dendrite.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index a3891ecbe1..f2cb36f717 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -650,22 +650,6 @@ async def call_stream( # Log outgoing request self._log_outgoing_request(synapse) - # Make the HTTP POST request - async def _make_stream_request(): - async with (await self.session).post( - url, - headers=synapse.to_headers(), - json=synapse.model_dump(), - timeout=aiohttp.ClientTimeout(total=timeout), - ) as response: - # Use synapse subclass' process_streaming_response method to yield the response chunks - async for chunk in synapse.process_streaming_response(response): # type: ignore - yield chunk # Yield each chunk as it's processed - json_response = synapse.extract_response_json(response) - - # Process the server response - self.process_server_response(response, json_response, synapse) - async with (await self.session).post( url, headers=synapse.to_headers(), From c1a64b3631eb9f7c3407bb0b873e43dfba605800 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:11:17 -0800 Subject: [PATCH 09/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index ade454263e..2f672c4590 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -23,15 +23,54 @@ def _retry_enabled() -> bool: def _retry_max_attempts() -> int: - return int(os.environ.get("BT_RETRY_MAX_ATTEMPTS", 3)) + """Get the maximum number of retry attempts from the environment, with validation.""" + default = 3 + raw = os.environ.get("BT_RETRY_MAX_ATTEMPTS") + if raw is None or raw == "": + return default + try: + return int(raw) + except (TypeError, ValueError): + logger.warning( + "Invalid value for BT_RETRY_MAX_ATTEMPTS=%r; falling back to default %d", + raw, + default, + ) + return default def _retry_base_delay() -> float: - return float(os.environ.get("BT_RETRY_BASE_DELAY", 1.0)) + """Get the base delay (in seconds) for retries from the environment, with validation.""" + default = 1.0 + raw = os.environ.get("BT_RETRY_BASE_DELAY") + if raw is None or raw == "": + return default + try: + return float(raw) + except (TypeError, ValueError): + logger.warning( + "Invalid value for BT_RETRY_BASE_DELAY=%r; falling back to default %.2f", + raw, + default, + ) + return default def _retry_max_delay() -> float: - return float(os.environ.get("BT_RETRY_MAX_DELAY", 60.0)) + """Get the maximum delay (in seconds) for retries from the environment, with validation.""" + default = 60.0 + raw = os.environ.get("BT_RETRY_MAX_DELAY") + if raw is None or raw == "": + return default + try: + return float(raw) + except (TypeError, ValueError): + logger.warning( + "Invalid value for BT_RETRY_MAX_DELAY=%r; falling back to default %.2f", + raw, + default, + ) + return default _RETRY_BACKOFF_FACTOR = 2.0 From 4661554b2e436a21a40fb2a4055d96ead41579aa Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:11:57 -0800 Subject: [PATCH 10/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 2f672c4590..5df73a21fc 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -76,9 +76,13 @@ def _retry_max_delay() -> float: _RETRY_BACKOFF_FACTOR = 2.0 +def _retry_backoff_factor() -> float: + return float(os.environ.get("BT_RETRY_BACKOFF_FACTOR", _RETRY_BACKOFF_FACTOR)) + + def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> float: """Calculates backoff time with exponential backoff and jitter.""" - delay = min(max_delay, base_delay * (_RETRY_BACKOFF_FACTOR**attempt)) + delay = min(max_delay, base_delay * (_retry_backoff_factor() ** attempt)) # Add jitter: random value between 0 and delay return delay * (0.5 + random.random()) From 3ff54cb58e60ba6528a4c2c366ad987ab0210c29 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:13:08 -0800 Subject: [PATCH 11/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 5df73a21fc..12556b929c 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -100,9 +100,74 @@ def retry_call( **kwargs, ) -> Any: """ - Synchronous retry wrapper. + Synchronous retry wrapper around ``func`` with optional exponential backoff. + + When the environment variable ``BT_RETRY_ENABLED`` is set to a truthy value + (e.g. ``"true"``, ``"1"``, ``"yes"``, ``"on"``), the call to ``func`` will be + retried on failure up to ``max_attempts`` times using exponential backoff with + jitter. When ``BT_RETRY_ENABLED`` is false or unset, ``func`` is executed + exactly once and any exception it raises is propagated immediately. + + Parameters + ---------- + func : Callable + The callable to be executed and potentially retried. + *args + Positional arguments forwarded to ``func``. + retry_exceptions : Exception type or tuple of Exception types, optional + Exception type(s) that should trigger a retry. Any exception that is + not an instance of these types is raised immediately without further + retry attempts. Defaults to ``(OSError, TimeoutError)``. + max_attempts : int, optional + Maximum number of attempts (initial attempt + retries) that will be + made before giving up. If ``None``, the value is taken from the + ``BT_RETRY_MAX_ATTEMPTS`` environment variable (default ``3``). + base_delay : float, optional + Base delay, in seconds, used for exponential backoff before applying + jitter. If ``None``, the value is taken from the + ``BT_RETRY_BASE_DELAY`` environment variable (default ``1.0``). + max_delay : float, optional + Maximum delay, in seconds, between attempts. The computed backoff + value will not exceed this. If ``None``, the value is taken from the + ``BT_RETRY_MAX_DELAY`` environment variable (default ``60.0``). + **kwargs + Keyword arguments forwarded to ``func``. + + Returns + ------- + Any + The return value of ``func`` from the first successful attempt. + + Raises + ------ + Exception + Any exception raised by ``func`` when retries are disabled + (``BT_RETRY_ENABLED`` is falsey), or when the exception type is not + included in ``retry_exceptions``. When retries are enabled and all + attempts fail with a ``retry_exceptions`` type, the last such + exception is re-raised after the final attempt. + + Examples + -------- + Basic usage with defaults:: + + result = retry_call(do_network_request, url, timeout=5) + + Custom retry configuration:: + + result = retry_call( + do_network_request, + url, + timeout=5, + max_attempts=5, + base_delay=0.5, + max_delay=10.0, + retry_exceptions=(OSError, TimeoutError, ConnectionError), + ) - If BT_RETRY_ENABLED is False, executes the function exactly once. + To disable retries entirely, unset ``BT_RETRY_ENABLED`` or set it to a + falsey value such as ``"false"`` or ``"0"``. In that case ``retry_call`` + simply calls ``func(*args, **kwargs)`` once and propagates any exception. """ if not _retry_enabled(): return func(*args, **kwargs) From cb81b59f5504789201e9b73de0bf3e70ebc51266 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:13:37 -0800 Subject: [PATCH 12/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 12556b929c..a280a7ec25 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -83,8 +83,8 @@ def _retry_backoff_factor() -> float: def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> float: """Calculates backoff time with exponential backoff and jitter.""" delay = min(max_delay, base_delay * (_retry_backoff_factor() ** attempt)) - # Add jitter: random value between 0 and delay - return delay * (0.5 + random.random()) + # Add jitter while ensuring the final backoff does not exceed max_delay + return min(max_delay, delay * (0.5 + random.random())) def retry_call( From 8ad8db01b8b98cf48354e036e3fa89ddfa8cede1 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:14:53 -0800 Subject: [PATCH 13/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 84 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index a280a7ec25..53e3ff6d6b 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -214,9 +214,89 @@ async def retry_async( **kwargs, ) -> Any: """ - Asynchronous retry wrapper. + Asynchronously call a function with optional retry logic and exponential backoff. - If BT_RETRY_ENABLED is False, executes the function exactly once. + When retries are **disabled** (``BT_RETRY_ENABLED`` is unset or set to a falsy + value such as ``"false"`` or ``"0"``), this helper makes a single call: + + * ``await func(*args, **kwargs)`` is executed exactly once. + * Any exception raised by ``func`` is propagated immediately. + + When retries are **enabled** (``BT_RETRY_ENABLED`` is set to a truthy value + such as ``"true"`` or ``"1"``), the call will be retried on a configurable + set of exceptions using exponential backoff with jitter: + + * The number of attempts defaults to ``BT_RETRY_MAX_ATTEMPTS`` (int, default 3) + and can be overridden via ``max_attempts``. + * The initial delay between attempts defaults to ``BT_RETRY_BASE_DELAY`` + (float seconds, default 1.0) and can be overridden via ``base_delay``. + * The delay is multiplied by an internal backoff factor on each attempt and + capped by ``BT_RETRY_MAX_DELAY`` (float seconds, default 60.0), which can + be overridden via ``max_delay``. + + Parameters + ---------- + func : Callable + Asynchronous callable to execute. This must return an awaitable and is + called as ``await func(*args, **kwargs)``. + *args : + Positional arguments forwarded to ``func`` on each attempt. + retry_exceptions : Exception type or tuple of Exception types, optional + Exception type(s) that trigger a retry when raised by ``func``. + Any exception not matching ``retry_exceptions`` is propagated immediately + without further retries. Defaults to ``(OSError, TimeoutError)``. + max_attempts : int, optional + Maximum number of attempts (including the first attempt). If ``None``, + the value is taken from the ``BT_RETRY_MAX_ATTEMPTS`` environment + variable (default 3 when unset). + base_delay : float, optional + Base delay in seconds before the first retry. If ``None``, the value is + taken from the ``BT_RETRY_BASE_DELAY`` environment variable (default + 1.0 when unset). + max_delay : float, optional + Maximum delay in seconds between retries. If ``None``, the value is + taken from the ``BT_RETRY_MAX_DELAY`` environment variable (default + 60.0 when unset). + **kwargs : + Keyword arguments forwarded to ``func`` on each attempt. + + Returns + ------- + Any + The result returned by ``func`` on the first successful attempt. + + Raises + ------ + Exception + Any exception raised by ``func`` when retries are disabled. + retry_exceptions + One of the configured ``retry_exceptions`` if all retry attempts are + exhausted while retries are enabled. + Exception + Any exception not matching ``retry_exceptions`` is propagated + immediately without retry. + + Examples + -------- + Basic usage with environment-controlled configuration:: + + async def fetch(): + ... + + result = await retry_async(fetch) + + Overriding retry configuration and the set of retryable exceptions:: + + async def fetch_with_timeout(): + ... + + result = await retry_async( + fetch_with_timeout, + retry_exceptions=(OSError, TimeoutError, ConnectionError), + max_attempts=5, + base_delay=0.5, + max_delay=10.0, + ) """ if not _retry_enabled(): return await func(*args, **kwargs) From 8c7ab7ae28d6407a61376f8c1d9b56f2a39c3754 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 01:34:56 -0800 Subject: [PATCH 14/40] Revert get_hyperparameter to use getattr --- bittensor/core/subtensor.py | 8381 +---------------------------------- 1 file changed, 90 insertions(+), 8291 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 8665e79327..1bc8097500 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,8324 +1,123 @@ -import copy -from datetime import datetime, timezone -from functools import lru_cache -from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Union, cast +""" +Implementation of the Subtensor class for interacting with the Bittensor blockchain. +""" -import scalecodec -from async_substrate_interface.errors import SubstrateRequestException -from async_substrate_interface.substrate_addons import RetrySyncSubstrate -from async_substrate_interface.sync_substrate import SubstrateInterface -from async_substrate_interface.types import ScaleObj -from async_substrate_interface.utils.storage import StorageKey -from bittensor_drand import get_encrypted_commitment -from bittensor_wallet.utils import SS58_FORMAT - -from bittensor.core.axon import Axon -from bittensor.core.chain_data import ( - CrowdloanConstants, - CrowdloanInfo, - DelegatedInfo, - DelegateInfo, - DynamicInfo, - MetagraphInfo, - NeuronInfo, - NeuronInfoLite, - ProposalVoteData, - ProxyAnnouncementInfo, - ProxyConstants, - ProxyInfo, - ProxyType, - RootClaimType, - SelectiveMetagraphIndex, - SimSwapResult, - StakeInfo, - SubnetHyperparameters, - SubnetIdentity, - SubnetInfo, - WeightCommitInfo, - decode_account_id, -) -from bittensor.core.chain_data.chain_identity import ChainIdentity -from bittensor.core.chain_data.utils import ( - decode_block, - decode_metadata, - decode_revealed_commitment, - decode_revealed_commitment_with_hotkey, -) -from bittensor.core.config import Config -from bittensor.core.errors import ChainError -from bittensor.core.extrinsics.children import ( - root_set_pending_childkey_cooldown_extrinsic, - set_children_extrinsic, -) -from bittensor.core.extrinsics.crowdloan import ( - contribute_crowdloan_extrinsic, - create_crowdloan_extrinsic, - dissolve_crowdloan_extrinsic, - finalize_crowdloan_extrinsic, - refund_crowdloan_extrinsic, - update_cap_crowdloan_extrinsic, - update_end_crowdloan_extrinsic, - update_min_contribution_crowdloan_extrinsic, - withdraw_crowdloan_extrinsic, -) -from bittensor.core.extrinsics.liquidity import ( - add_liquidity_extrinsic, - modify_liquidity_extrinsic, - remove_liquidity_extrinsic, - toggle_user_liquidity_extrinsic, -) -from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic -from bittensor.core.extrinsics.move_stake import ( - move_stake_extrinsic, - swap_stake_extrinsic, - transfer_stake_extrinsic, -) -from bittensor.core.extrinsics.proxy import ( - add_proxy_extrinsic, - announce_extrinsic, - create_pure_proxy_extrinsic, - kill_pure_proxy_extrinsic, - poke_deposit_extrinsic, - proxy_announced_extrinsic, - proxy_extrinsic, - reject_announcement_extrinsic, - remove_announcement_extrinsic, - remove_proxies_extrinsic, - remove_proxy_extrinsic, -) -from bittensor.core.extrinsics.registration import ( - burned_register_extrinsic, - register_extrinsic, - register_subnet_extrinsic, - set_subnet_identity_extrinsic, -) -from bittensor.core.extrinsics.root import ( - claim_root_extrinsic, - root_register_extrinsic, - set_root_claim_type_extrinsic, -) -from bittensor.core.extrinsics.serving import ( - publish_metadata_extrinsic, - serve_axon_extrinsic, -) -from bittensor.core.extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, - set_auto_stake_extrinsic, -) -from bittensor.core.extrinsics.start_call import start_call_extrinsic -from bittensor.core.extrinsics.take import set_take_extrinsic -from bittensor.core.extrinsics.transfer import transfer_extrinsic -from bittensor.core.extrinsics.unstaking import ( - unstake_all_extrinsic, - unstake_extrinsic, - unstake_multiple_extrinsic, -) -from bittensor.core.extrinsics.utils import get_transfer_fn_params -from bittensor.core.extrinsics.weights import ( - commit_timelocked_weights_extrinsic, - commit_weights_extrinsic, - reveal_weights_extrinsic, - set_weights_extrinsic, -) -from bittensor.core.metagraph import Metagraph -from bittensor.core.settings import ( - DEFAULT_MEV_PROTECTION, - DEFAULT_PERIOD, - MLKEM768_PUBLIC_KEY_SIZE, - TAO_APP_BLOCK_EXPLORER, - TYPE_REGISTRY, - version_as_int, -) -from bittensor.core.types import ( - BlockInfo, - ExtrinsicResponse, - Salt, - SubtensorMixin, - UIDs, - Weights, -) -from bittensor.utils import ( - Certificate, - decode_hex_identity_dict, - format_error_message, - get_caller_name, - get_mechid_storage_index, - is_valid_ss58_address, - u16_normalized_float, - u64_normalized_float, - validate_max_attempts, -) -from bittensor.utils.balance import ( - Balance, - FixedPoint, - check_balance_amount, - fixed_to_float, -) -from bittensor.utils.btlogging import logging -from bittensor.utils.liquidity import ( - LiquidityPosition, - calculate_fees, - get_fees, - price_to_tick, - tick_to_price, -) +import asyncio +import logging +from typing import Any, Optional, TYPE_CHECKING, Union +from bittensor.core.errors import NetworkQueryError +from bittensor.core.settings import version_as_int +from bittensor.utils.btlogging import logging as logger +from bittensor.utils.registration import POWSolution if TYPE_CHECKING: - from async_substrate_interface.sync_substrate import QueryMapResult - from bittensor_wallet import Keypair, Wallet - from scalecodec.types import GenericCall - - -class Subtensor(SubtensorMixin): - """Synchronous interface for interacting with the Bittensor blockchain. - - This class provides a thin layer over the Substrate Interface offering synchronous functionality for Bittensor. This - includes frequently-used calls for querying blockchain data, managing stakes and liquidity positions, registering - neurons, submitting weights, and many other functions for participating in Bittensor. - - Notes: - Key Bittensor concepts used throughout this class: - - - **Coldkey**: The key pair corresponding to a user's overall wallet. Used to transfer, stake, manage subnets. - - **Hotkey**: A key pair (each wallet may have zero, one, or more) used for neuron operations (mining and - validation). - - **Netuid**: Unique identifier for a subnet (0 is the Root Subnet) - - **UID**: Unique identifier for a neuron registered to a hotkey on a specific subnet. - - **Metagraph**: Data structure containing the complete state of a subnet at a block. - - **TAO**: The base network token; subnet 0 stake is in TAO - - **Alpha**: Subnet-specific token representing some quantity of TAO staked into a subnet. - - **Rao**: Smallest unit of TAO (1 TAO = 1e9 Rao) - - Bittensor Glossary - - Wallets, Coldkeys and Hotkeys in Bittensor - + from bittensor.core.chain_data import ( + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + SubnetInfo, + SubnetHyperparameters, + IPInfo, + StakeInfo, + DelegateInfo, + ScheduledColdkeySwapInfo, + ) + + +class Subtensor: + """ + The Subtensor class provides an interface for interacting with the Bittensor blockchain. + + This class handles all substrate interactions and provides methods for querying + blockchain state and submitting extrinsics. """ def __init__( self, network: Optional[str] = None, - config: Optional[Config] = None, - log_verbose: bool = False, - fallback_endpoints: Optional[list[str]] = None, - retry_forever: bool = False, - archive_endpoints: Optional[list[str]] = None, - mock: bool = False, - ): - """Initializes a Subtensor instance for blockchain interaction. - - Parameters: - network: The network name to connect to (e.g., `finney` for Bittensor mainnet, `test`, for - Bittensor test network, `local` for a locally deployed blockchain). If `None`, uses the - default network from config. - config: Configuration object for the Subtensor instance. If `None`, uses the default configuration. - log_verbose: Enables or disables verbose logging. - fallback_endpoints: List of fallback WebSocket endpoints to use if the primary network endpoint is - unavailable. These are tried in order when the default endpoint fails. - retry_forever: Whether to retry connection attempts indefinitely on connection errors. - mock: Whether this is a mock instance. FOR TESTING ONLY. - archive_endpoints: List of archive node endpoints for queries requiring historical block data beyond the - retention period of lite nodes. These are only used when requesting blocks that the current node is - unable to serve. - - Returns: - None - """ - if config is None: - config = self.config() - self._config = copy.deepcopy(config) - self.chain_endpoint, self.network = self.setup_config(network, self._config) - - self.log_verbose = log_verbose - self._check_and_log_network_settings() - - logging.debug( - f"Connecting to network: [blue]{self.network}[/blue], " - f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..." - ) - self.substrate = self._get_substrate( - fallback_endpoints=fallback_endpoints, - retry_forever=retry_forever, - _mock=mock, - archive_endpoints=archive_endpoints, - ) - if self.log_verbose: - logging.set_trace() - logging.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - def close(self): - """Closes the connection to the blockchain. - - Use this to explicitly clean up resources and close the network connection instead of waiting for garbage - collection. - - Returns: - None - - Example: - - sub = bt.Subtensor(network="finney") - - # calls to subtensor - - sub.close() - - """ - self.substrate.close() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - # Helpers ========================================================================================================== - - def _decode_crowdloan_entry( - self, - crowdloan_id: int, - data: dict, - block_hash: Optional[str] = None, - ) -> "CrowdloanInfo": - """ - Internal helper to parse and decode a single Crowdloan record. - - Automatically decodes the embedded `call` field if present (Inline SCALE format). - """ - call_data = data.get("call") - if call_data and "Inline" in call_data: - try: - inline_bytes = bytes(call_data["Inline"][0][0]) - decoded_call = self.substrate.create_scale_object( - type_string="Call", - data=scalecodec.ScaleBytes(inline_bytes), - block_hash=block_hash, - ).decode() - data["call"] = decoded_call - except Exception as e: - data["call"] = {"decode_error": str(e), "raw": call_data} - - return CrowdloanInfo.from_dict(crowdloan_id, data) - - @lru_cache(maxsize=128) - def _get_block_hash(self, block_id: int): - return self.substrate.get_block_hash(block_id) - - def _get_substrate( - self, - fallback_endpoints: Optional[list[str]] = None, - retry_forever: bool = False, + config: Optional[dict] = None, _mock: bool = False, - archive_endpoints: Optional[list[str]] = None, - ) -> Union[SubstrateInterface, RetrySyncSubstrate]: - """Creates the Substrate instance based on provided arguments. - - This internal method creates either a standard SubstrateInterface or a RetrySyncSubstrate depending on - whether fallback/archive endpoints or infinite retry is requested. - - When `fallback_endpoints`, `archive_endpoints`, or `retry_forever` are provided, a RetrySyncSubstrate - is created with automatic failover and exponential backoff retry logic. Otherwise, a standard - SubstrateInterface is used. - - Parameters: - fallback_endpoints: List of fallback WebSocket endpoints to use if the primary endpoint is unavailable. - retry_forever: Whether to retry connection attempts indefinitely on connection errors. - _mock: Whether this is a mock instance. Used primarily for testing purposes. - archive_endpoints: List of archive node endpoints for historical block queries. Archive nodes maintain full - block history, while lite nodes only keep recent blocks. Use these when querying blocks older than the - lite node's retention period (typically a few thousand blocks). - - Returns: - Either SubstrateInterface (simple connection) or RetrySyncSubstrate (with failover and retry logic). - """ - if fallback_endpoints or retry_forever or archive_endpoints: - return RetrySyncSubstrate( - url=self.chain_endpoint, - ss58_format=SS58_FORMAT, - type_registry=TYPE_REGISTRY, - use_remote_preset=True, - chain_name="Bittensor", - fallback_chains=fallback_endpoints, - retry_forever=retry_forever, - _mock=_mock, - archive_nodes=archive_endpoints, - ) - return SubstrateInterface( - url=self.chain_endpoint, - ss58_format=SS58_FORMAT, - type_registry=TYPE_REGISTRY, - use_remote_preset=True, - chain_name="Bittensor", - _mock=_mock, - ) - - def determine_block_hash(self, block: Optional[int]) -> Optional[str]: - """Determine the block hash for the block specified with the provided parameters. - - Ensures that only one of the block specification parameters is used and returns the appropriate block hash - for blockchain queries. - - Parameters: - block: The block number to get the hash for. If `None`, returns `None`. - - Returns: - The block hash (hex string with `0x` prefix) if one can be determined, `None` otherwise. - - Notes: - - - """ - if block is None: - return None - else: - return self.get_block_hash(block=block) - - def _runtime_method_exists(self, api: str, method: str, block_hash: str) -> bool: - """ - Check if a runtime call method exists at the given block. - - The complicated logic here comes from the fact that there are two ways in which runtime calls - are stored: the new and primary method is through the Metadata V15, but the V14 is a good backup (implemented - around mid 2024) - - Returns: - True if the runtime call method exists, False otherwise. - """ - runtime = self.substrate.init_runtime(block_hash=block_hash) - if runtime.metadata_v15 is not None: - metadata_v15_value = runtime.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} - try: - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - _ = methods[method] - return True - except KeyError: - return False - else: - try: - self.substrate.get_metadata_runtime_call_function( - api=api, - method=method, - block_hash=block_hash, - ) - return True - except ValueError: - return False - - def _query_with_fallback( - self, - *args: tuple[str, str, Optional[list[Any]]], - block_hash: Optional[str] = None, - default_value: Any = ValueError, ): """ - Queries the subtensor node with a given set of args, falling back to the next group if the method - does not exist at the given block. This method exists to support backwards compatibility for blocks. - - Parameters: - *args: Tuples containing (module, storage_function, params) in the order they should be attempted. - block_hash: The hash of the block being queried. If not provided, the chain tip will be used. - default_value: The default value to return if none of the methods exist at the given block. - - Returns: - The value returned by the subtensor node, or the default value if none of the methods exist at the given - block. - - Raises: - ValueError: If no default value is provided, and none of the methods exist at the given block, a - ValueError will be raised. - - Example: - - value = self._query_with_fallback( - - # the first attempt will be made to SubtensorModule.MechanismEmissionSplit with params [1] - - ("SubtensorModule", "MechanismEmissionSplit", [1]), - - # if it does not exist at the given block, the next attempt will be made to - - # SubtensorModule.MechanismEmission with params None - - ("SubtensorModule", "MechanismEmission", None), - - block_hash="0x1234", - - # if none of the methods exist at the given block, the default value of None will be returned - - default_value=None, - - ) - + Initialize the Subtensor instance. + + Args: + network: Network name to connect to + config: Configuration dictionary + _mock: Whether to use mock substrate interface """ - if block_hash is None: - block_hash = self.substrate.get_chain_head() - for module, storage_function, params in args: - if self.substrate.get_metadata_storage_function( - module_name=module, - storage_name=storage_function, - block_hash=block_hash, - ): - return self.substrate.query( - module=module, - storage_function=storage_function, - block_hash=block_hash, - params=params, - ) - if not isinstance(default_value, ValueError): - return default_value - else: - raise default_value - - def _runtime_call_with_fallback( + self.network = network + self.config = config or {} + self._mock = _mock + self.substrate = None + + def _get_hyperparameter( self, - *args: tuple[str, str, Optional[list[Any]] | dict[str, Any]], - block_hash: Optional[str] = None, - default_value: Any = ValueError, - ): - """ - Makes a runtime call to the subtensor node with a given set of args, falling back to the next group if the - api.method does not exist at the given block. This method exists to support backwards compatibility for blocks. - - Parameters: - *args: Tuples containing (api, method, params) in the order they should be attempted. - block_hash: The hash of the block being queried. If not provided, the chain tip will be used. - default_value: The default value to return if none of the methods exist at the given block. - - Raises: - ValueError: If no default value is provided, and none of the methods exist at the given block, a - ValueError will be raised. - - Example: - - query = self._runtime_call_with_fallback( - - # the first attempt will be made to SubnetInfoRuntimeApi.get_selective_mechagraph with the - - # given params - - ( - - "SubnetInfoRuntimeApi", - - "get_selective_mechagraph", - - [netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]], - - ), - - # if it does not exist at the given block, the next attempt will be made as such: - - ("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]), - - block_hash=block_hash, - - # if none of the methods exist at the given block, the default value will be returned - - default_value=None, - - ) - - """ - if block_hash is None: - block_hash = self.substrate.get_chain_head() - for api, method, params in args: - if self._runtime_method_exists( - api=api, method=method, block_hash=block_hash - ): - return self.substrate.runtime_call( - api=api, - method=method, - block_hash=block_hash, - params=params, - ) - if not isinstance(default_value, ValueError): - return default_value - else: - raise default_value - - def get_hyperparameter( - self, param_name: str, netuid: int, block: Optional[int] = None + param_name: str, + netuid: int, + block: Optional[int] = None, + reuse_block: bool = False, ) -> Optional[Any]: - """Retrieves a specified hyperparameter for a specific subnet. - - This method queries the blockchain for subnet-specific hyperparameters such as difficulty, tempo, immunity - period, and other network configuration values. Return types and units vary by parameter. - - Parameters: - param_name: The name of the hyperparameter storage function to retrieve. - netuid: The unique identifier of the subnet. - block: The block number to query. If `None`, queries the current chain head. - + """ + Internal method to retrieve a hyperparameter for a specific subnet. + + Args: + param_name: Name of the hyperparameter to retrieve + netuid: Unique identifier for the subnet + block: Block number to query at (None for latest) + reuse_block: Whether to reuse cached block + Returns: - The value of the specified hyperparameter if the subnet exists, `None` otherwise. Return type varies - by parameter (int, float, bool, or Balance). - - Notes: - - + The hyperparameter value, or None if not found """ - block_hash = self.determine_block_hash(block) - if not self.subnet_exists(netuid, block=block): - logging.error(f"subnet {netuid} does not exist") - return None - + if self.substrate is None: + raise RuntimeError("Substrate connection not initialized") + result = self.substrate.query( module="SubtensorModule", storage_function=param_name, params=[netuid], - block_hash=block_hash, - ) - - if result is None: - return None - - if hasattr(result, "value"): - return result.value - - return result - - @property - def block(self) -> int: - """Provides an asynchronous getter to retrieve the current block number. - - Returns: - The current blockchain block number. - """ - return self.get_current_block() - - def sim_swap( - self, - origin_netuid: int, - destination_netuid: int, - amount: "Balance", - block: Optional[int] = None, - ) -> SimSwapResult: - """Simulates a swap/stake operation and calculates the fees and resulting amounts. - - This method queries the SimSwap Runtime API to calculate the swap fees (in Alpha or TAO) and the quantities - of Alpha or TAO tokens expected as output from the transaction. This simulation does NOT include the - blockchain extrinsic transaction fee (the fee to submit the transaction itself). - - When moving stake between subnets, the operation may involve swapping Alpha (subnet-specific stake token) to - TAO (the base network token), then TAO to Alpha on the destination subnet. For subnet 0 (root network), all - stake is in TAO. - - Parameters: - origin_netuid: Netuid of the source subnet (0 if adding stake). - destination_netuid: Netuid of the destination subnet. - amount: Amount to swap/stake as a Balance object. Use `Balance.from_tao(...)` or - `Balance.from_rao(...)` to create the amount. - block: The block number to query. If `None`, uses the current chain head. - - Returns: - SimSwapResult: Object containing `alpha_fee`, `tao_fee`, `alpha_amount`, and `tao_amount` fields - representing the swap fees and output amounts. - - Example: - - # Simulate staking 100 TAO stake to subnet 1 - - result = subtensor.sim_swap( - - origin_netuid=0, - - destination_netuid=1, - - amount=Balance.from_tao(100) - - ) - - print(f"Fee: {result.tao_fee.tao} TAO, Output: {result.alpha_amount} Alpha") - - Notes: - - **Alpha**: Subnet-specific stake token (dynamic TAO) - - **TAO**: Base network token; subnet 0 uses TAO directly - - The returned fees do NOT include the extrinsic transaction fee - - - Transaction Fees: - - Glossary: - """ - check_balance_amount(amount) - if origin_netuid > 0 and destination_netuid > 0: - # for cross-subnet moves where neither origin nor destination is root - intermediate_result_ = self.query_runtime_api( - runtime_api="SwapRuntimeApi", - method="sim_swap_alpha_for_tao", - params={"netuid": origin_netuid, "alpha": amount.rao}, - block=block, - ) - sn_price = self.get_subnet_price(origin_netuid, block=block) - intermediate_result = SimSwapResult.from_dict( - intermediate_result_, origin_netuid - ) - result = SimSwapResult.from_dict( - self.query_runtime_api( - runtime_api="SwapRuntimeApi", - method="sim_swap_tao_for_alpha", - params={ - "netuid": destination_netuid, - "tao": intermediate_result.tao_amount.rao, - }, - block=block, - ), - origin_netuid, - ) - secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) - result.alpha_fee = result.alpha_fee + secondary_fee - return result - elif origin_netuid > 0: - # dynamic to tao - return SimSwapResult.from_dict( - self.query_runtime_api( - runtime_api="SwapRuntimeApi", - method="sim_swap_alpha_for_tao", - params={"netuid": origin_netuid, "alpha": amount.rao}, - block=block, - ), - origin_netuid, - ) - else: - # tao to dynamic or unstaked to staked tao (SN0) - return SimSwapResult.from_dict( - self.query_runtime_api( - runtime_api="SwapRuntimeApi", - method="sim_swap_tao_for_alpha", - params={"netuid": destination_netuid, "tao": amount.rao}, - block=block, - ), - destination_netuid, - ) - - # Subtensor queries =========================================================================================== - - def query_constant( - self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleObj"]: - """Retrieves a constant from the specified module on the Bittensor blockchain. - - Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot - be accessed through other, standard getter methods. - - Parameters: - module_name: The name of the module containing the constant (e.g., `Balances`, `SubtensorModule`). - constant_name: The name of the constant to retrieve (e.g., `ExistentialDeposit`). - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A SCALE-decoded object if found, `None` otherwise. Access the actual value using `.value` attribute. - Common types include int (for counts/blocks), Balance objects (for amounts in Rao), and booleans. - - """ - return self.substrate.get_constant( - module_name=module_name, - constant_name=constant_name, - block_hash=self.determine_block_hash(block), - ) - - def query_map( - self, - module: str, - name: str, - params: Optional[list] = None, - block: Optional[int] = None, - ) -> "QueryMapResult": - """Queries map storage from any module on the Bittensor blockchain. - - Use this function for nonstandard queries to map storage defined within the Bittensor blockchain, if these cannot - be accessed through other, standard getter methods. - - Parameters: - module: The name of the module from which to query the map storage (e.g., "SubtensorModule", "System"). - name: The specific storage function within the module to query (e.g., "Bonds", "Weights"). - params: Parameters to be passed to the query. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - QueryMapResult: A data structure representing the map storage if found, None otherwise. - """ - result = self.substrate.query_map( - module=module, - storage_function=name, - params=params, - block_hash=self.determine_block_hash(block=block), - ) - return result - - def query_map_subtensor( - self, - name: str, - params: Optional[list] = None, - block: Optional[int] = None, - ) -> "QueryMapResult": - """Queries map storage from the Subtensor module on the Bittensor blockchain. - - Use this function for nonstandard queries to map storage defined within the Bittensor blockchain, if these cannot - be accessed through other, standard getter methods. - - Parameters: - name: The name of the map storage function to query. - params: A list of parameters to pass to the query function. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - An object containing the map-like data structure, or `None` if not found. - """ - return self.substrate.query_map( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=self.determine_block_hash(block), - ) - - def query_module( - self, - module: str, - name: str, - params: Optional[list] = None, - block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any, FixedPoint]]: - """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. - This function is a generic query interface that allows for flexible and diverse data retrieval from various - blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor - blockchain, if these cannot be accessed through other, standard getter methods. - - Parameters: - module: The name of the module from which to query data. - name: The name of the storage function within the module. - params: A list of parameters to pass to the query function. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - An object containing the requested data if found, `None` otherwise. - - """ - return self.substrate.query( - module=module, - storage_function=name, - params=params, - block_hash=self.determine_block_hash(block), + block_hash=None if block is None else self.substrate.get_block_hash(block), + reuse_block_hash=reuse_block, ) - - def query_runtime_api( - self, - runtime_api: str, - method: str, - params: Optional[Union[list[Any], dict[str, Any]]] = None, - block: Optional[int] = None, - ) -> Any: - """Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime - and retrieve data encoded in Scale Bytes format. Use this function for nonstandard queries to the runtime - environment, if these cannot be accessed through other, standard getter methods. - - Parameters: - runtime_api: The name of the runtime API to query. - method: The specific method within the runtime API to call. - params: The parameters to pass to the method call. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The decoded result from the runtime API call, or `None` if the call fails. - - """ - block_hash = self.determine_block_hash(block) - - return self.substrate.runtime_call( - api=runtime_api, - method=method, - params=params, - block_hash=block_hash, - ).value - - def query_subtensor( + + return getattr(result, "value", result) + + def get_subnet_hyperparameters( self, - name: str, - params: Optional[list] = None, + netuid: int, block: Optional[int] = None, - ) -> Optional[Union["ScaleObj", Any]]: - """Queries named storage from the Subtensor module on the Bittensor blockchain. - - Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot - be accessed through other, standard getter methods. - - Parameters: - name: The name of the storage function to query. - params: A list of parameters to pass to the query function. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - query_response: An object containing the requested data. - """ - return self.substrate.query( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=self.determine_block_hash(block), - ) - - def state_call( - self, method: str, data: str, block: Optional[int] = None - ) -> dict[Any, Any]: - """Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. - This function is typically used for advanced, nonstandard queries not provided by other getter methods. - - Use this method when you need to query runtime APIs or storage functions that don't have dedicated - wrapper methods in the SDK. For standard queries, prefer the specific getter methods (e.g., `get_balance`, - `get_stake`) which provide better type safety and error handling. - - Parameters: - method: The runtime API method name (e.g., "SubnetInfoRuntimeApi", "get_metagraph"). - data: Hex-encoded string of the SCALE-encoded parameters to pass to the method. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The result of the rpc call. - - """ - block_hash = self.determine_block_hash(block) - return self.substrate.rpc_request( - method="state_call", params=[method, data], block_hash=block_hash - ) - - # Common subtensor calls =========================================================================================== - - def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo"]]: - """Queries the blockchain for comprehensive information about all subnets, including their dynamic parameters - and operational status. - - Parameters: - block: The block number to query. If `None`, queries the current chain head. - - Returns: - Optional[list[DynamicInfo]]: A list of `DynamicInfo` objects, each containing detailed information about - a subnet, or None if the query fails. - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_all_dynamic_info", - block_hash=block_hash, - ) - decoded = query.decode() - try: - subnet_prices = self.get_subnet_prices(block=block) - for sn in decoded: - sn.update( - {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} - ) - except (SubstrateRequestException, ValueError) as e: - logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") - - return DynamicInfo.list_from_dicts(decoded) - - def blocks_since_last_step( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """Queries the blockchain to determine how many blocks have passed since the last epoch step for a specific - subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The number of blocks since the last step in the subnet, or None if the query fails. - - Notes: - - - """ - query = self.query_subtensor( - name="BlocksSinceLastStep", block=block, params=[netuid] - ) - return query.value if query is not None and hasattr(query, "value") else query - - def blocks_since_last_update( - self, netuid: int, uid: int, block: Optional[int] = None - ) -> Optional[int]: - """Returns the number of blocks since the last update, or `None` if the subnetwork or UID does not exist. - - Parameters: - netuid: The unique identifier of the subnetwork. - uid: The unique identifier of the neuron. - block: The block number for this query. If `None`, queries the current chain head. - - Returns: - The number of blocks since the last update, or None if the subnetwork or UID does not exist. + reuse_block: bool = False, + ) -> Optional["SubnetHyperparameters"]: """ - block = block or self.get_current_block() - call = self.get_hyperparameter( - param_name="LastUpdate", netuid=netuid, block=block - ) - return None if not call else (block - int(call[uid])) - - def blocks_until_next_epoch( - self, netuid: int, tempo: Optional[int] = None, block: Optional[int] = None - ) -> Optional[int]: - """Returns the number of blocks until the next epoch of subnet with provided netuid. - - Parameters: - netuid: The unique identifier of the subnetwork. - tempo: The tempo of the subnet. - block: the block number for this query. - + Retrieve hyperparameters for a specific subnet. + + Args: + netuid: Unique identifier for the subnet + block: Block number to query at (None for latest) + reuse_block: Whether to reuse cached block + Returns: - The number of blocks until the next epoch of the subnet with provided netuid. + SubnetHyperparameters object or None if subnet doesn't exist """ - block = block or self.block - - tempo = tempo or self.tempo(netuid=netuid) - if not tempo: - return None - - # the logic is the same as in SubtensorModule:blocks_until_next_epoch - netuid_plus_one = int(netuid) + 1 - tempo_plus_one = tempo + 1 - adjusted_block = (block + netuid_plus_one) % (2**64) - remainder = adjusted_block % tempo_plus_one - return tempo - remainder - - def bonds( + pass + + def get_commitment( self, netuid: int, - mechid: int = 0, + uid: int, block: Optional[int] = None, - ) -> list[tuple[int, list[tuple[int, int]]]]: - """Retrieves the bond distribution set by subnet validators within a specific subnet. - - Bonds represent a validator's accumulated assessment of each miner's performance over time, which serves as the - starting point of Yuma Consensus. - - Parameters: - netuid: Subnet identifier. - mechid: Subnet mechanism identifier (default 0 for primary mechanism). - block: The block number for this query. If `None`, queries the current chain head. - - Returns: - List of tuples, where each tuple contains: - - validator_uid: The UID of the validator - - bonds: List of (miner_uid, bond_value) pairs - - Bond values are u16-normalized (0-65535, where 65535 = 1.0 or 100%). - - Example: - - # Get bonds for subnet 1 - - bonds = subtensor.bonds(netuid=1) - - print(bonds[0]) - - # example output: (5, [(0, 32767), (1, 16383), (3, 8191)]) - - # This means validator UID 5 has bonds: 50% to miner 0, 25% to miner 1, 12.5% to miner 3 - - Notes: - - See: - - See: - """ - storage_index = get_mechid_storage_index(netuid, mechid) - b_map_encoded = self.substrate.query_map( - module="SubtensorModule", - storage_function="Bonds", - params=[storage_index], - block_hash=self.determine_block_hash(block), - ) - b_map = [] - for uid, b in b_map_encoded: - if b.value is not None: - b_map.append((uid, b.value)) - - return b_map - - def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> bool: - """Check if commit-reveal mechanism is enabled for a given subnet at a specific block. - - Parameters: - netuid: The unique identifier of the subnet for which to check the commit-reveal mechanism. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - True if commit-reveal mechanism is enabled, False otherwise. - - Notes: - - - - + ) -> Optional[str]: """ - call = self.get_hyperparameter( - param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid - ) - return True if call is True else False - - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - - This parameter determines the computational challenge required for neurons to participate in consensus and - validation processes, using proof of work (POW) registration. - - Parameters: - netuid: The unique identifier of the subnet. - block: The block number to query. If `None`, queries the current chain head. - + Retrieve the commitment for a specific neuron. + + Args: + netuid: Unique identifier for the subnet + uid: Unique identifier for the neuron + block: Block number to query at (None for latest) + Returns: - The value of the 'Difficulty' hyperparameter if the subnet exists, `None` otherwise. - - Notes: - Burn registration is much more common on Bittensor subnets currently, compared to POW registration. - - - - - - - + The commitment string, or None if not found """ - call = self.get_hyperparameter( - param_name="Difficulty", netuid=netuid, block=block - ) - if call is None: - return None - return int(call) - - def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """Returns true if the hotkey has been associated with a coldkey through account creation. - - This method queries the Subtensor's Owner storage map to check if the hotkey has been paired with a - coldkey, as it must be before it (the hotkey) can be used for neuron registration. - - The Owner storage map defaults to the zero address (`5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM`) - for unused hotkeys. This method returns `True` if the Owner value is anything other than this default. - - Parameters: - hotkey_ss58: The SS58 address of the hotkey. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - True if the hotkey has been associated with a coldkey, False otherwise. - - Notes: - - - """ - result = self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - return_val = ( - False - if result is None - # not the default key (0x0) - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val - - def get_admin_freeze_window(self, block: Optional[int] = None) -> int: - """Returns the duration, in blocks, of the administrative freeze window at the end of each epoch. - - The admin freeze window is a period at the end of each epoch during which subnet owner - operations are prohibited. This prevents subnet owners from modifying hyperparameters or performing certain - administrative actions right before validators submit weights at the epoch boundary. - - Parameters: - block: The block number to query. - - Returns: - The number of blocks in the administrative freeze window (default: 10 blocks, ~2 minutes). - - Notes: - - - """ - - return self.substrate.query( - module="SubtensorModule", - storage_function="AdminFreezeWindow", - block_hash=self.determine_block_hash(block), - ).value - - def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: - """Retrieves detailed information about all subnets within the Bittensor network. - - Parameters: - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A list of SubnetInfo objects, each containing detailed information about a subnet. - - """ - result = self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnets_info_v2", - params=[], - block=block, - ) - if not result: - return [] - try: - subnets_prices = self.get_subnet_prices(block=block) - - for subnet in result: - subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)}) - except (SubstrateRequestException, ValueError) as e: - logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") - - return SubnetInfo.list_from_dicts(result) - - def get_all_commitments( - self, netuid: int, block: Optional[int] = None - ) -> dict[str, str]: - """Retrieves raw commitment metadata from a given subnet. - - This method retrieves all commitment data for all neurons in a specific subnet. This is useful for analyzing the - commit-reveal patterns across an entire subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A mapping of the ss58:commitment with the commitment as a string. - - Example: - - # TODO add example of how to handle realistic commitment data - """ - query = self.query_map( - module="Commitments", - name="CommitmentOf", - params=[netuid], - block=block, - ) - result = {} - for id_, value in query: - try: - result[decode_account_id(id_[0])] = decode_metadata(value) - except Exception as error: - logging.error( - f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" - ) - return result - - def get_all_ema_tao_inflow( - self, - block: Optional[int] = None, - ) -> dict[int, tuple[int, Balance]]: - """Retrieves the EMA (exponential moving average) of net TAO flows for all subnets. - - The EMA tracks net TAO flows (staking minus unstaking) with a 30-day half-life (~86.8 day window), smoothing - out short-term fluctuations while capturing sustained staking trends. This metric determines the subnet's share - of TAO emissions under the current, flow-based model. Positive values indicate net inflow (more staking than unstaking), - negative values indicate net outflow. Subnets with negative EMA flows receive zero emissions. - - Parameters: - block: The block number to retrieve the commitment from. - - Returns: - Dict mapping netuid to (last_updated_block, ema_flow). The Balance represents the EMA of net TAO flow in - TAO units. Positive values indicate sustained net inflow, negative values indicate sustained net outflow. - - The EMA uses a smoothing factor α ≈ 0.000003209, creating a 30-day half-life and ~86.8 day window. Only - direct stake/unstake operations count toward flows; neuron registrations and root claims are excluded. - Subnet 0 (root network) does not have an EMA TAO flow value. - - Notes: - - Flow-based emissions: - - EMA smoothing: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query_map( - module="SubtensorModule", - storage_function="SubnetEmaTaoFlow", - block_hash=block_hash, - ) - tao_inflow_ema = {} - for netuid, (block_updated, tao_bits) in query: - ema_value = int(fixed_to_float(tao_bits)) - tao_inflow_ema[netuid] = (block_updated, Balance.from_rao(ema_value)) - return tao_inflow_ema - - def get_all_metagraphs_info( - self, - all_mechanisms: bool = False, - block: Optional[int] = None, - ) -> Optional[list[MetagraphInfo]]: - """ - Retrieves a list of MetagraphInfo objects for all subnets - - Parameters: - all_mechanisms: If True then returns all mechanisms, otherwise only those with index 0 for all subnets. - block: The blockchain block number for the query. - - Returns: - List of MetagraphInfo objects for all existing subnets. - - Notes: - - - """ - block_hash = self.determine_block_hash(block) - method = "get_all_mechagraphs" if all_mechanisms else "get_all_metagraphs" - query = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method=method, - block_hash=block_hash, - ) - if query is None or not hasattr(query, "value"): - return None - - return MetagraphInfo.list_from_dicts(query.value) - - def get_all_neuron_certificates( - self, netuid: int, block: Optional[int] = None - ) -> dict[str, Certificate]: - """ - Retrieves the TLS certificates for neurons within a specified subnet (netuid) of the Bittensor network. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - Dictionary mapping neuron hotkey SS58 addresses to their Certificate objects. Only includes neurons - that have registered certificates. - - Notes: - This method is used for certificate discovery to establish mutual TLS communication between neurons. - - - - """ - query_certificates = self.query_map( - module="SubtensorModule", - name="NeuronCertificates", - params=[netuid], - block=block, - ) - output = {} - for key, item in query_certificates: - output[decode_account_id(key)] = Certificate(item.value) - return output - - def get_all_revealed_commitments( - self, netuid: int, block: Optional[int] = None - ) -> dict[str, tuple[tuple[int, str], ...]]: - """Retrieves all revealed commitments for a given subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A dictionary mapping hotkey addresses to tuples of (reveal_block, commitment_message) pairs. - Each validator can have multiple revealed commitments (up to 10 most recent). - - Example: - - # sample return value - - { - - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), - - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), - - } - - Notes: - - - - """ - query = self.query_map( - module="Commitments", - name="RevealedCommitments", - params=[netuid], - block=block, - ) - - result = {} - for pair in query: - hotkey_ss58_address, commitment_message = ( - decode_revealed_commitment_with_hotkey(pair) - ) - result[hotkey_ss58_address] = commitment_message - return result - - def get_all_subnets_netuid(self, block: Optional[int] = None) -> UIDs: - """ - Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. - - Parameters: - block: The blockchain block number for the query. - - Returns: - A list of subnet netuids. - - This function provides a comprehensive view of the subnets within the Bittensor network, - offering insights into its diversity and scale. - """ - result = self.substrate.query_map( - module="SubtensorModule", - storage_function="NetworksAdded", - block_hash=self.determine_block_hash(block), - ) - subnets = [] - if result.records: - for netuid, exists in result: - if exists: - subnets.append(netuid) - return subnets - - def get_auto_stakes( - self, - coldkey_ss58: str, - block: Optional[int] = None, - ) -> dict[int, str]: - """Fetches auto stake destinations for a given wallet across all subnets. - - Parameters: - coldkey_ss58: Coldkey ss58 address. - block: The block number for the query. If `None`, queries the current chain head. - - Returns: - Dictionary mapping netuid to hotkey, where: - - - netuid: The unique identifier of the subnet. - - hotkey: The hotkey of the wallet. - - Notes: - - - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.query_map( - module="SubtensorModule", - storage_function="AutoStakeDestination", - params=[coldkey_ss58], - block_hash=block_hash, - ) - - pairs = {} - for netuid, destination in query: - hotkey_ss58 = decode_account_id(destination.value[0]) - if hotkey_ss58: - pairs[int(netuid)] = hotkey_ss58 - - return pairs - - def get_balance(self, address: str, block: Optional[int] = None) -> Balance: - """Retrieves the balance for given coldkey. - - This method queries the System module's Account storage to get the current balance of a coldkey address. The - balance represents the amount of TAO tokens held by the specified address. - - Parameters: - address: The coldkey address in SS58 format. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - Balance: The balance object containing the account's TAO balance. - """ - balance = self.substrate.query( - module="System", - storage_function="Account", - params=[address], - block_hash=self.determine_block_hash(block), - ) - return Balance(balance["data"]["free"]) - - def get_balances( - self, - *addresses: str, - block: Optional[int] = None, - ) -> dict[str, Balance]: - """Retrieves the balance for given coldkey(s). - - This method efficiently queries multiple coldkey addresses in a single batch operation, returning a dictionary - mapping each address to its corresponding balance. This is more efficient than calling get_balance multiple - times. - - Parameters: - *addresses: Variable number of coldkey addresses in SS58 format. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A dictionary mapping each address to its Balance object. - """ - if not (block_hash := self.determine_block_hash(block)): - block_hash = self.substrate.get_chain_head() - calls = [ - ( - self.substrate.create_storage_key( - "System", "Account", [address], block_hash=block_hash - ) - ) - for address in addresses - ] - batch_call = self.substrate.query_multi(calls, block_hash=block_hash) - results = {} - for item in batch_call: - value = item[1] or {"data": {"free": 0}} - results.update({item[0].params[0]: Balance(value["data"]["free"])}) - return results - - def get_current_block(self) -> int: - """Returns the current block number on the Bittensor blockchain. - - This function provides the latest block number, indicating the most recent state of the blockchain. - - Returns: - int: The current chain block number. - - Notes: - - - """ - return self.substrate.get_block_number(None) - - def get_block_hash(self, block: Optional[int] = None) -> str: - """Retrieves the hash of a specific block on the Bittensor blockchain. - - The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its - integrity and immutability. It is a fundamental aspect of blockchain technology, providing a secure reference - to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the - trustworthiness of the blockchain. - - Parameters: - block: The block number for which the hash is to be retrieved. If `None`, returns the latest block hash. - - Returns: - str: The cryptographic hash of the specified block. - - Notes: - - - """ - if block is not None: - return self._get_block_hash(block) - else: - return self.substrate.get_chain_head() - - def get_block_info( - self, - block: Optional[int] = None, - block_hash: Optional[str] = None, - ) -> Optional[BlockInfo]: - """Retrieve complete information about a specific block from the Subtensor chain. - - This method aggregates multiple low-level RPC calls into a single structured response, returning both the raw - on-chain data and high-level decoded metadata for the given block. - - Parameters: - block: The block number for which the hash is to be retrieved. - block_hash: The hash of the block to retrieve the block from. - - Returns: - BlockInfo instance: A dataclass containing all available information about the specified block, including: - - - number: The block number. - - hash: The corresponding block hash. - - timestamp: The timestamp of the block (based on the `Timestamp.Now` extrinsic). - - header: The raw block header returned by the node RPC. - - extrinsics: The list of decoded extrinsics included in the block. - - explorer: The link to block explorer service. Always related with finney block data. - """ - block_info = self.substrate.get_block( - block_number=block, block_hash=block_hash, ignore_decoding_errors=True - ) - if isinstance(block_info, dict) and (header := block_info.get("header")): - block = block or header.get("number", None) - block_hash = block_hash or header.get("hash", None) - extrinsics = cast(list, block_info.get("extrinsics")) - timestamp = None - for ext in extrinsics: - if ext.value_serialized["call"]["call_module"] == "Timestamp": - timestamp = ext.value_serialized["call"]["call_args"][0]["value"] - break - return BlockInfo( - number=block, - hash=block_hash, - timestamp=timestamp, - header=header, - extrinsics=extrinsics, - explorer=f"{TAO_APP_BLOCK_EXPLORER}{block}", - ) - return None - - def get_children( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> tuple[bool, list[tuple[float, str]], str]: - """Retrieves the children of a given hotkey and netuid. - - This method queries the SubtensorModule's ChildKeys storage function to get the children and formats them before - returning as a tuple. It provides information about the child neurons that a validator has set for weight - distribution. - - Parameters: - hotkey_ss58: The hotkey value. - netuid: The netuid value. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A tuple containing a boolean indicating success or failure, a list of formatted children with their - proportions, and an error message (if applicable). - - Example: - - # Get children for a hotkey in subnet 1 - - success, children, error = subtensor.get_children(hotkey="5F...", netuid=1) - - if success: - - for proportion, child_hotkey in children: - - print(f"Child {child_hotkey}: {proportion}") - - Notes: - - - """ - try: - children = self.substrate.query( - module="SubtensorModule", - storage_function="ChildKeys", - params=[hotkey_ss58, netuid], - block_hash=self.determine_block_hash(block), - ) - if children: - formatted_children = [] - for proportion, child in children.value: - # Convert U64 to int - formatted_child = decode_account_id(child[0]) - normalized_proportion = u64_normalized_float(proportion) - formatted_children.append((normalized_proportion, formatted_child)) - return True, formatted_children, "" - else: - return True, [], "" - except SubstrateRequestException as e: - return False, [], format_error_message(e) - - def get_children_pending( - self, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> tuple[ - list[tuple[float, str]], - int, - ]: - """Retrieves the pending children of a given hotkey and netuid. - - This method queries the SubtensorModule's PendingChildKeys storage function to get children that are pending - approval or in a cooldown period. These are children that have been proposed but not yet finalized. - - Parameters: - hotkey_ss58: The hotkey value. - netuid: The netuid value. - block: The block number for which the children are to be retrieved. If `None`, queries the current chain head. - - Returns: - tuple: A tuple containing: - - - list[tuple[float, str]]: A list of children with their proportions. - - int: The cool-down block number. - - Notes: - - - """ - - children, cooldown = self.substrate.query( - module="SubtensorModule", - storage_function="PendingChildKeys", - params=[netuid, hotkey_ss58], - block_hash=self.determine_block_hash(block), - ).value - - return ( - [ - ( - u64_normalized_float(proportion), - decode_account_id(child[0]), - ) - for proportion, child in children - ], - cooldown, - ) - - def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: - """Retrieves the on-chain commitment for a specific neuron in the Bittensor network. - - This method retrieves the commitment data that a neuron has published to the blockchain. Commitments are used in - the commit-reveal mechanism for secure weight setting and other network operations. - - Parameters: - netuid: The unique identifier of the subnetwork. - uid: The unique identifier of the neuron. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The commitment data as a string. - - - # TODO: add a real example of how to handle realistic commitment data, or chop example - - Notes: - - - """ - metagraph = self.metagraph(netuid) - try: - hotkey = metagraph.hotkeys[uid] # type: ignore - except IndexError: - logging.error( - "Your uid is not in the hotkeys. Please double-check your UID." - ) - return "" - - metadata = cast(dict, self.get_commitment_metadata(netuid, hotkey, block)) - try: - return decode_metadata(metadata) - except Exception as error: - logging.error(error) - return "" - - def get_commitment_metadata( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Union[str, dict]: - # TODO: how to handle return data? need good example @roman - """Fetches raw commitment metadata from specific subnet for given hotkey. - - Parameters: - netuid: The unique subnet identifier. - hotkey_ss58: The hotkey ss58 address. - block: The blockchain block number for the query. - - Returns: - The raw commitment metadata. Returns a dict when commitment data exists, - or an empty string when no commitment is found for the given hotkey on the subnet. - - Notes: - - - """ - commit_data = self.substrate.query( - module="Commitments", - storage_function="CommitmentOf", - params=[netuid, hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - return commit_data - - def get_crowdloan_constants( - self, - constants: Optional[list[str]] = None, - block: Optional[int] = None, - ) -> "CrowdloanConstants": - """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. - - If a list of constant names is provided, only those constants will be queried. - Otherwise, all known constants defined in `CrowdloanConstants.field_names()` are fetched. - - These constants define requirements and operational limits for crowdloan campaigns: - - - `AbsoluteMinimumContribution`: Minimum amount per contribution (TAO). - - `MaxContributors`: Maximum number of unique contributors per crowdloan. - - `MaximumBlockDuration`: Maximum duration (in blocks) for a crowdloan campaign (60 days = 432,000 blocks on production). - - `MinimumDeposit`: Minimum deposit required from the creator (TAO). - - `MinimumBlockDuration`: Minimum duration (in blocks) for a crowdloan campaign (7 days = 50,400 blocks on production). - - `RefundContributorsLimit`: Maximum number of contributors refunded per `refund_crowdloan` call (typically 50). - - Parameters: - constants: Specific constant names to query. If `None`, retrieves all constants from `CrowdloanConstants`. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - A `CrowdloanConstants` data object containing the queried constants. Missing constants return `None`. - Notes: - These constants enforce contribution floors, duration bounds, and refund batching limits. - - - Crowdloans Overview: - - """ - result = {} - const_names = constants or CrowdloanConstants.constants_names() - - for const_name in const_names: - query = self.query_constant( - module_name="Crowdloan", - constant_name=const_name, - block=block, - ) - - if query is not None: - result[const_name] = query.value - - return CrowdloanConstants.from_dict(result) - - def get_crowdloan_contributions( - self, - crowdloan_id: int, - block: Optional[int] = None, - ) -> dict[str, "Balance"]: - """Retrieves all contributions made to a specific crowdloan campaign. - - Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. - - Parameters: - crowdloan_id: The unique identifier of the crowdloan. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - Dictionary mapping contributor SS58 addresses to their `Balance` contribution amounts (in Rao). - Returns empty dictionary if the crowdloan has no contributions or does not exist. - - Notes: - Contributions are clipped to the remaining cap. Once the cap is reached, no further contributions are accepted. - - - Crowdloans Overview: - - Crowdloan Tutorial: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query_map( - module="Crowdloan", - storage_function="Contributions", - params=[crowdloan_id], - block_hash=block_hash, - ) - result = {} - for record in query.records: - if record[1].value: - result[decode_account_id(record[0])] = Balance.from_rao(record[1].value) - return result - - def get_crowdloan_by_id( - self, crowdloan_id: int, block: Optional[int] = None - ) -> Optional["CrowdloanInfo"]: - """Retrieves detailed information about a specific crowdloan campaign. - - Parameters: - crowdloan_id: Unique identifier of the crowdloan (auto-incremented starting from 0). - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - `CrowdloanInfo` object containing: campaign ID, creator address, creator's deposit, - minimum contribution amount, end block, funding cap, funds account address, amount raised, - optional target address, optional embedded call, finalization status, and contributor count. - Returns `None` if the crowdloan does not exist. - - Notes: - - - Crowdloans Overview: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query( - module="Crowdloan", - storage_function="Crowdloans", - params=[crowdloan_id], - block_hash=block_hash, - ) - if not query: - return None - return self._decode_crowdloan_entry( - crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash - ) - - def get_crowdloan_next_id( - self, - block: Optional[int] = None, - ) -> int: - """Retrieves the next available crowdloan identifier. - - Crowdloan IDs are allocated sequentially starting from 0. This method returns the ID that will be - assigned to the next crowdloan created via :meth:`create_crowdloan`. - - Parameters: - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The next crowdloan ID (integer) to be assigned. - - Notes: - - Crowdloans Overview: - - Crowdloan Tutorial: - """ - block_hash = self.determine_block_hash(block) - result = self.substrate.query( - module="Crowdloan", - storage_function="NextCrowdloanId", - block_hash=block_hash, - ) - return int(result.value or 0) - - def get_crowdloans( - self, - block: Optional[int] = None, - ) -> list["CrowdloanInfo"]: - """Retrieves all existing crowdloan campaigns with their metadata. - - Returns comprehensive information for all crowdloans registered on the blockchain, including - both active and finalized campaigns. - - Parameters: - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - List of `CrowdloanInfo` objects, each containing: campaign ID, creator address, creator's deposit, - minimum contribution amount, end block, funding cap, funds account address, amount raised, - optional target address, optional embedded call, finalization status, and contributor count. - Returns empty list if no crowdloans exist. - - Notes: - - Crowdloans Overview: - - Crowdloan Lifecycle: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query_map( - module="Crowdloan", - storage_function="Crowdloans", - block_hash=block_hash, - ) - - crowdloans = [] - - for c_id, value_obj in getattr(query, "records", []): - data = value_obj.value - if not data: - continue - crowdloans.append( - self._decode_crowdloan_entry( - crowdloan_id=c_id, data=data, block_hash=block_hash - ) - ) - - return crowdloans - - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional["DelegateInfo"]: - """Retrieves detailed information about a delegate neuron (validator) based on its hotkey. This function - provides a comprehensive view of the delegate's status, including its stakes, nominators, and reward - distribution. - - Parameters: - hotkey_ss58: The `SS58` address of the delegate's hotkey. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - Detailed information about the delegate neuron, `None` if not found. - - Notes: - - - - - - """ - - result = self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegate", - params=[hotkey_ss58], - block=block, - ) - - if not result: - return None - - return DelegateInfo.from_dict(result) - - def get_delegate_identities( - self, block: Optional[int] = None - ) -> dict[str, ChainIdentity]: - """Fetches delegate identities. - - Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method - retrieves the on-chain identity information for all delegates, including display name, legal name, web URLs, - and other metadata they have set. - - Parameters: - block: The block number to query. If `None`, queries the current chain head. - - Returns: - Dictionary mapping delegate SS58 addresses to their ChainIdentity objects. - - Notes: - - - """ - identities = self.substrate.query_map( - module="SubtensorModule", - storage_function="IdentitiesV2", - block_hash=self.determine_block_hash(block), - ) - - return { - decode_account_id(ss58_address[0]): ChainIdentity.from_dict( - decode_hex_identity_dict(identity.value), - ) - for ss58_address, identity in identities - } - - def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> float: - """ - Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the - percentage of rewards that the delegate claims from its nominators' stakes. - - Parameters: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - float: The delegate take percentage. - - Notes: - - - """ - result = self.query_subtensor( - name="Delegates", - block=block, - params=[hotkey_ss58], - ) - - return u16_normalized_float(result.value) # type: ignore - - def get_delegated( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> list[DelegatedInfo]: - """Retrieves delegates and their associated stakes for a given nominator coldkey. - - This method identifies all delegates (validators) that a specific coldkey has staked tokens to, along with - stake amounts and other delegation information. This is useful for account holders to understand their stake - allocations and involvement in the network's delegation and consensus mechanisms. - - Parameters: - coldkey_ss58: The SS58 address of the account's coldkey. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - List of DelegatedInfo objects containing stake amounts and delegate information. Returns empty list if no - delegations exist for the coldkey. - - Notes: - - - """ - - result = self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegated", - params=[coldkey_ss58], - block=block, - ) - - if not result: - return [] - - return DelegatedInfo.list_from_dicts(result) - - def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: - """Fetches all delegates registered on the chain. - - Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method - retrieves comprehensive information about all delegates including their hotkeys, total stake, nominator count, - take percentage, and other metadata. - - Parameters: - block: The block number to query. If `None`, queries the current chain head. - - Returns: - List of DelegateInfo objects containing comprehensive delegate information. Returns empty list if no - delegates are registered. - - Notes: - - - """ - result = self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegates", - params=[], - block=block, - ) - if result: - return DelegateInfo.list_from_dicts(result) - else: - return [] - - def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: - """Retrieves the existential deposit amount for the Bittensor blockchain. - - The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. - Accounts with balances below this threshold can be reaped (removed) to conserve network resources and prevent - blockchain bloat from dust accounts. - - Parameters: - block: The blockchain block number for the query. - - Returns: - The existential deposit amount in RAO. - - Notes: - - - """ - result = self.substrate.get_constant( - module_name="Balances", - constant_name="ExistentialDeposit", - block_hash=self.determine_block_hash(block), - ) - - if result is None: - raise Exception("Unable to retrieve existential deposit amount.") - - return Balance.from_rao(getattr(result, "value", 0)) - - def get_ema_tao_inflow( - self, - netuid: int, - block: Optional[int] = None, - ) -> Optional[tuple[int, Balance]]: - """Retrieves the EMA (exponential moving average) of net TAO flow for a specific subnet. - - The EMA tracks net TAO flows (staking minus unstaking) with a 30-day half-life (~86.8 day window), smoothing - out short-term fluctuations while capturing sustained staking trends. This metric determines the subnet's share - of TAO emissions under the current, flow-based model. Positive values indicate net inflow (more staking than unstaking), - negative values indicate net outflow. Subnets with negative EMA flows receive zero emissions. - - Parameters: - netuid: The unique identifier of the subnet to query. - block: The block number to query. If `None`, uses latest finalized block. - - Returns: - Tuple of (last_updated_block, ema_flow) where ema_flow is the EMA of net TAO flow in TAO units. - Returns `None` if the subnet does not exist or if querying subnet 0 (root network). - - The EMA uses a smoothing factor α ≈ 0.000003209, creating a 30-day half-life and ~86.8 day window. Only direct - stake/unstake operations count toward flows; neuron registrations and root claims are excluded. Subnet 0 (root - network) does not have an EMA TAO flow value and will return `None`. - - Notes: - - Flow-based emissions: - - EMA smoothing: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query( - module="SubtensorModule", - storage_function="SubnetEmaTaoFlow", - params=[netuid], - block_hash=block_hash, - ) - - # sn0 doesn't have EmaTaoInflow - if query is None: - return None - - block_updated, tao_bits = query.value - ema_value = int(fixed_to_float(tao_bits)) - return block_updated, Balance.from_rao(ema_value) - - def get_hotkey_owner( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[str]: - """ - Retrieves the owner of the given hotkey at a specific block hash. - This function queries the blockchain for the owner of the provided hotkey. If the hotkey does not exist at the - specified block hash, it returns `None`. - - Parameters: - hotkey_ss58: The SS58 address of the hotkey. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The SS58 address of the owner if the hotkey exists, or `None` if it doesn't. - """ - hk_owner_query = self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - exists = False - if hk_owner_query: - exists = self.does_hotkey_exist(hotkey_ss58, block=block) - hotkey_owner = hk_owner_query if exists else None - return hotkey_owner - - def get_last_bonds_reset( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ): - """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. - - Parameters: - netuid: The network uid to fetch from. - hotkey_ss58: The hotkey of the neuron for which to fetch the last bonds reset. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The block number when bonds were last reset, or `None` if no bonds reset has occurred. - - Notes: - - - - - """ - return self.substrate.query( - module="Commitments", - storage_function="LastBondsReset", - params=[netuid, hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - - def get_last_commitment_bonds_reset_block( - self, - netuid: int, - uid: int, - block: Optional[int] = None, - ) -> Optional[int]: - """ - Retrieves the last block number when the bonds reset were triggered by publish_metadata for a specific neuron. - - Parameters: - netuid: The unique identifier of the subnetwork. - uid: The unique identifier of the neuron. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - The block number when the bonds were last reset, or `None` if not found. - """ - - metagraph = self.metagraph(netuid, block=block) - try: - hotkey_ss58 = metagraph.hotkeys[uid] - except IndexError: - logging.error( - "Your uid is not in the hotkeys. Please double-check your UID." - ) - return None - block_data = self.get_last_bonds_reset(netuid, hotkey_ss58, block) - try: - return decode_block(block_data) - except TypeError: - return None - - def get_liquidity_list( - self, - wallet: "Wallet", - netuid: int, - block: Optional[int] = None, - ) -> Optional[list[LiquidityPosition]]: - """ - Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). - Calculates associated fee rewards based on current global and tick-level fee data. - - Parameters: - wallet: Wallet instance to fetch positions for. - netuid: Subnet unique id. - block: The blockchain block number for the query. - - Returns: - List of liquidity positions, or None if subnet does not exist. - """ - if not self.subnet_exists(netuid=netuid): - logging.debug(f"Subnet {netuid} does not exist.") - return None - - if not self.is_subnet_active(netuid=netuid): - logging.debug(f"Subnet {netuid} is not active.") - return None - - # Fetch positions - positions_response = self.query_map( - module="Swap", - name="Positions", - block=block, - params=[netuid, wallet.coldkeypub.ss58_address], - ) - if len(positions_response.records) == 0: - return [] - - block_hash = self.determine_block_hash(block) - - # Fetch global fees and current price - fee_global_tao_query_sk = self.substrate.create_storage_key( - pallet="Swap", - storage_function="FeeGlobalTao", - params=[netuid], - block_hash=block_hash, - ) - fee_global_alpha_query_sk = self.substrate.create_storage_key( - pallet="Swap", - storage_function="FeeGlobalAlpha", - params=[netuid], - block_hash=block_hash, - ) - sqrt_price_query_sk = self.substrate.create_storage_key( - pallet="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], - block_hash=block_hash, - ) - fee_global_tao_query, fee_global_alpha_query, sqrt_price_query = ( - self.substrate.query_multi( - storage_keys=[ - fee_global_tao_query_sk, - fee_global_alpha_query_sk, - sqrt_price_query_sk, - ], - block_hash=block_hash, - ) - ) - - fee_global_tao = fixed_to_float(fee_global_tao_query[1]) - fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) - sqrt_price = fixed_to_float(sqrt_price_query[1]) - current_tick = price_to_tick(sqrt_price**2) - - positions_values: list[tuple[dict, int, int]] = [] - positions_storage_keys: list[StorageKey] = [] - for _, p in positions_response: - position = p.value - - tick_low_idx = position["tick_low"][0] - tick_high_idx = position["tick_high"][0] - - tick_low_sk = self.substrate.create_storage_key( - pallet="Swap", - storage_function="Ticks", - params=[netuid, tick_low_idx], - block_hash=block_hash, - ) - tick_high_sk = self.substrate.create_storage_key( - pallet="Swap", - storage_function="Ticks", - params=[netuid, tick_high_idx], - block_hash=block_hash, - ) - positions_values.append((position, tick_low_idx, tick_high_idx)) - positions_storage_keys.extend([tick_low_sk, tick_high_sk]) - # query all our ticks at once - ticks_query = self.substrate.query_multi( - positions_storage_keys, block_hash=block_hash - ) - # iterator with just the values - ticks = iter([x[1] for x in ticks_query]) - positions = [] - for position, tick_low_idx, tick_high_idx in positions_values: - tick_low = next(ticks) - tick_high = next(ticks) - - # Calculate fees above/below range for both tokens - tao_below = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_low_idx, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - tao_above = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_high_idx, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - alpha_below = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_low_idx, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - alpha_above = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_high_idx, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - - # Calculate fees earned by position - fees_tao, fees_alpha = calculate_fees( - position=position, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - tao_fees_below_low=tao_below, - tao_fees_above_high=tao_above, - alpha_fees_below_low=alpha_below, - alpha_fees_above_high=alpha_above, - netuid=netuid, - ) - - positions.append( - LiquidityPosition( - **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } - ) - ) - - return positions - - def get_mechanism_emission_split( - self, netuid: int, block: Optional[int] = None - ) -> Optional[list[int]]: - """Returns the emission percentages allocated to each subnet mechanism. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - A list of integers representing the percentage of emission allocated to each subnet mechanism (rounded to - whole numbers). Returns None if emission is evenly split or if the data is unavailable. - """ - block_hash = self.determine_block_hash(block) - module = "SubtensorModule" - storage_function = "MechanismEmissionSplit" - if not self.substrate.get_metadata_storage_function( - module, storage_function, block_hash=block_hash - ): - return None - result = self.substrate.query( - module="SubtensorModule", - storage_function="MechanismEmissionSplit", - params=[netuid], - block_hash=block_hash, - ) - if result is None or not hasattr(result, "value"): - return None - - return [round(i / sum(result.value) * 100) for i in result.value] - - def get_mechanism_count( - self, - netuid: int, - block: Optional[int] = None, - ) -> int: - """Retrieves the number of mechanisms for the given subnet. - - Parameters: - netuid: Subnet identifier. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The number of mechanisms for the given subnet. - - Notes: - - - """ - block_hash = self.determine_block_hash(block) - module = "SubtensorModule" - storage_function = "MechanismCountCurrent" - if not self.substrate.get_metadata_storage_function( - module, storage_function, block_hash=block_hash - ): - return 1 - query = self.substrate.query( - module=module, - storage_function=storage_function, - params=[netuid], - block_hash=block_hash, - ) - return query.value if query is not None and hasattr(query, "value") else 1 - - def get_metagraph_info( - self, - netuid: int, - mechid: int = 0, - selected_indices: Optional[ - Union[list[SelectiveMetagraphIndex], list[int]] - ] = None, - block: Optional[int] = None, - ) -> Optional[MetagraphInfo]: - """Retrieves full or partial metagraph information for the specified subnet (netuid). - - A metagraph is a data structure that contains comprehensive information about the current state of a subnet, - including detailed information on all the nodes (neurons) such as subnet validator stakes and subnet weights - and bonds. - - Parameters: - netuid: Subnet unique identifier. - mechid: Subnet mechanism unique identifier. - selected_indices: Optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. - If not provided, all available fields will be returned. - block: The block number at which to query the data. If `None`, queries the current chain head. - - Returns: - MetagraphInfo object with the requested subnet mechanism data, None if the subnet mechanism does not exist. - - Example: - - # Retrieve all fields from the metagraph from subnet 2 mechanism 0 - - meta_info = subtensor.get_metagraph_info(netuid=2) - - # Retrieve all fields from the metagraph from subnet 2 mechanism 1 - - meta_info = subtensor.get_metagraph_info(netuid=2, mechid=1) - - # Retrieve selective data from the metagraph from subnet 2 mechanism 0 - - partial_meta_info = subtensor.get_metagraph_info( - - netuid=2, - - selected_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] - ) - - # Retrieve selective data from the metagraph from subnet 2 mechanism 1 - - partial_meta_info = subtensor.get_metagraph_info( - netuid=2, - mechid=1, - selected_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] - ) - - Notes: - - - - """ - - block_hash: str = ( - self.determine_block_hash(block=block) or self.substrate.get_chain_head() - ) - - # Normalize selected_indices to a list of integers - if selected_indices is not None: - indexes = [ - f.value if isinstance(f, SelectiveMetagraphIndex) else f - for f in selected_indices - ] - if 0 not in indexes: - indexes = [0] + indexes - query = self._runtime_call_with_fallback( - ( - "SubnetInfoRuntimeApi", - "get_selective_mechagraph", - [netuid, mechid, indexes], - ), - ("SubnetInfoRuntimeApi", "get_selective_metagraph", [netuid, indexes]), - block_hash=block_hash, - default_value=ValueError( - "You have specified `selected_indices` to retrieve metagraph info selectively, but the " - "selective runtime calls are not available at this block (probably too old). Do not specify " - "`selected_indices` to retrieve metagraph info selectively." - ), - ) - else: - query = self._runtime_call_with_fallback( - ( - "SubnetInfoRuntimeApi", - "get_selective_mechagraph", - [netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]], - ), - ("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]), - block_hash=block_hash, - default_value=None, - ) - - if query is None or not hasattr(query, "value") or query.value is None: - logging.error( - f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." - ) - return None - - return MetagraphInfo.from_dict(query.value) - - def get_mev_shield_current_key( - self, block: Optional[int] = None - ) -> Optional[bytes]: - """ - Retrieves the CurrentKey from the MevShield pallet storage. - - The CurrentKey contains the ML-KEM-768 public key that is currently being used for encryption in this block. - This key is rotated from NextKey at the beginning of each block. - - Parameters: - block: The blockchain block number at which to perform the query. If None, uses the current block. - - Returns: - The ML-KEM-768 public key as bytes (1184 bytes for ML-KEM-768) - - Note: - If CurrentKey is not set (None in storage), this function returns None. This can happen if no validator has - announced a key yet. - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( - module="MevShield", - storage_function="CurrentKey", - block_hash=block_hash, - ) - - if query is None: - return None - - public_key_bytes = bytes(next(iter(query))) - - # Validate public_key size for ML-KEM-768 - if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: - raise ValueError( - f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " - f"Expected exactly {MLKEM768_PUBLIC_KEY_SIZE} bytes." - ) - - return public_key_bytes - - def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes]: - """ - Retrieves the NextKey from the MevShield pallet storage. - - The NextKey contains the ML-KEM-768 public key that will be used for encryption in the next block. This key is - rotated from NextKey to CurrentKey at the beginning of each block. - - Parameters: - block: The blockchain block number at which to perform the query. If None, uses the current block. - - Returns: - The ML-KEM-768 public key as bytes (1184 bytes for ML-KEM-768) - - Note: - If NextKey is not set (None in storage), this function returns None. This can happen if no validator has - announced the next key yet. - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.query( - module="MevShield", - storage_function="NextKey", - block_hash=block_hash, - ) - - if query is None: - return None - - public_key_bytes = bytes(next(iter(query))) - - # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) - if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: - raise ValueError( - f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " - f"Expected exactly {MLKEM768_PUBLIC_KEY_SIZE} bytes." - ) - - return public_key_bytes - - def get_mev_shield_submission( - self, - submission_id: str, - block: Optional[int] = None, - ) -> Optional[dict[str, str | int | bytes]]: - """ - Retrieves Submission from the MevShield pallet storage. - - If submission_id is provided, returns a single submission. If submission_id is None, returns all submissions from - the storage map. - - Parameters: - submission_id: The hash ID of the submission. Can be a hex string with "0x" prefix or bytes. If None, - returns all submissions. - block: The blockchain block number at which to perform the query. If None, uses the current block. - - Returns: - If submission_id is provided: A dictionary containing the submission data if found, None otherwise. The - dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core (as hex string with "0x" prefix) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - If submission_id is None: A dictionary mapping submission IDs (as hex strings) to submission dictionaries. - - Note: - If a specific submission does not exist in storage, this function returns None. If querying all submissions - and none exist, returns an empty dictionary. - """ - block_hash = self.determine_block_hash(block=block) - submission_id = ( - submission_id[2:] if submission_id.startswith("0x") else submission_id - ) - submission_id_bytes = bytes.fromhex(submission_id) - - query = self.substrate.query( - module="MevShield", - storage_function="Submissions", - params=[submission_id_bytes], - block_hash=block_hash, - ) - - if query is None or not isinstance(query, dict): - return None - - autor = decode_account_id(query.get("author")) - commitment = bytes(query.get("commitment")[0]) - ciphertext = bytes(query.get("ciphertext")[0]) - submitted_in = query.get("submitted_in") - - return { - "author": autor, - "commitment": commitment, - "ciphertext": ciphertext, - "submitted_in": submitted_in, - } - - def get_mev_shield_submissions( - self, - block: Optional[int] = None, - ) -> Optional[dict[str, dict[str, str | int]]]: - """ - Retrieves all encrypted submissions from the MevShield pallet storage. - - This function queries the MevShield.Submissions storage map and returns all pending encrypted submissions that - have been submitted via submit_encrypted but not yet executed via execute_revealed. - - Parameters: - block: The blockchain block number for the query. If None, uses the current block. - - Returns: - A dictionary mapping wrapper_id (as hex string with "0x" prefix) to submission data dictionaries. Each - submission dictionary contains: - - author: The SS58 address of the account that submitted the encrypted extrinsic - - commitment: The blake2_256 hash of the payload_core as bytes (32 bytes) - - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) - - submitted_in: The block number when the submission was created - - Returns None if no submissions exist in storage at the specified block. - - Note: - Submissions are automatically pruned after KEY_EPOCH_HISTORY blocks (100 blocks) by the pallet's - on_initialize hook. Only submissions that have been submitted but not yet executed will be present in - storage. - """ - block_hash = self.determine_block_hash(block=block) - query = self.substrate.query_map( - module="MevShield", - storage_function="Submissions", - block_hash=block_hash, - ) - - result = {} - for q in query: - key, value = q - value = value.value - result["0x" + bytes(key[0]).hex()] = { - "author": decode_account_id(value.get("author")), - "commitment": bytes(value.get("commitment")[0]), - "ciphertext": bytes(value.get("ciphertext")[0]), - "submitted_in": value.get("submitted_in"), - } - - return result if result else None - - def get_minimum_required_stake(self) -> Balance: - """Returns the minimum required stake threshold for nominator cleanup operations. - - This threshold is used ONLY for cleanup after unstaking operations. If a nominator's remaining stake - falls below this minimum after an unstake, the remaining stake is forcefully cleared and returned - to the coldkey to prevent dust accounts. - - This is NOT the minimum checked during staking operations. The actual minimum for staking is determined - by DefaultMinStake (typically 0.001 TAO plus fees). - - Returns: - The minimum stake threshold as a Balance object. Nominator stakes below this amount - are automatically cleared after unstake operations. - - Notes: - - - """ - result = self.substrate.query( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - - return Balance.from_rao(getattr(result, "value", 0)) - - def get_netuids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> list[int]: - """Retrieves a list of subnet UIDs (netuids) where a given hotkey is a member. This function identifies the - specific subnets within the Bittensor network where the neuron associated with the hotkey is active. - - Parameters: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - A list of netuids where the neuron is a member. - - Notes: - - - """ - result = self.substrate.query_map( - module="SubtensorModule", - storage_function="IsNetworkMember", - params=[hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - netuids = [] - if result.records: - for record in result: - if record[1].value: - netuids.append(record[0]) - return netuids - - def get_neuron_certificate( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[Certificate]: - """ - Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a specified - subnet (netuid) of the Bittensor network. - - Parameters: - hotkey_ss58: The SS58 address of the neuron's hotkey. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - Certificate object containing the neuron's TLS public key and algorithm, or `None` if the neuron has - not registered a certificate. - - This function is used for certificate discovery for setting up mutual tls communication between neurons. - """ - certificate_query = self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block=block, - params=[netuid, hotkey_ss58], - ) - try: - if certificate_query: - certificate = cast(dict, certificate_query) - return Certificate(certificate) - except AttributeError: - return None - return None - - def get_neuron_for_pubkey_and_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional["NeuronInfo"]: - """ - Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID - (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor - network. - - Parameters: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - Optional: Detailed information about the neuron if found, `None` otherwise. - - This function is crucial for accessing specific neuron data and understanding its status, stake, and other - attributes within a particular subnet of the Bittensor ecosystem. - """ - block_hash = self.determine_block_hash(block) - uid_query = self.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], - block_hash=block_hash, - ) - if (uid := getattr(uid_query, "value", None)) is None: - return NeuronInfo.get_null_neuron() - - return self.neuron_for_uid( - uid=uid, - netuid=netuid, - block=block, - ) - - def get_next_epoch_start_block( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Calculates the first block number of the next epoch for the given subnet. - - If `block` is not provided, the current chain block will be used. Epochs are determined based on the subnet's - tempo (i.e., blocks per epoch). The result is the block number at which the next epoch will begin. - - Parameters: - netuid: The unique identifier of the subnet. - block: The reference block to calculate from. If None, uses the current chain block height. - - Returns: - int: The block number at which the next epoch will start, or None if tempo is 0 or invalid. - - Notes: - - - """ - tempo = self.tempo(netuid=netuid, block=block) - current_block = block or self.block - - if not tempo: - return None - - blocks_until = self.blocks_until_next_epoch( - netuid=netuid, tempo=tempo, block=current_block - ) - - if blocks_until is None: - return None - - return current_block + blocks_until + 1 - - def get_owned_hotkeys( - self, - coldkey_ss58: str, - block: Optional[int] = None, - ) -> list[str]: - """ - Retrieves all hotkeys owned by a specific coldkey address. - - Parameters: - coldkey_ss58: The SS58 address of the coldkey to query. - block: The blockchain block number for the query. - - Returns: - list[str]: A list of hotkey SS58 addresses owned by the coldkey. - """ - block_hash = self.determine_block_hash(block) - owned_hotkeys = self.substrate.query( - module="SubtensorModule", - storage_function="OwnedHotkeys", - params=[coldkey_ss58], - block_hash=block_hash, - ) - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] - - def get_parents( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> list[tuple[float, str]]: - """ - This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys - storage function to get the children and formats them before returning as a tuple. - - Parameters: - hotkey_ss58: The child hotkey SS58. - netuid: The netuid value. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A list of formatted parents [(proportion, parent)] - - Notes: - - - - :meth:`get_children` for retrieving child keys - """ - parents = self.substrate.query( - module="SubtensorModule", - storage_function="ParentKeys", - params=[hotkey_ss58, netuid], - block_hash=self.determine_block_hash(block), - ) - if parents: - formatted_parents = [] - for proportion, parent in parents.value: - # Convert U64 to int - formatted_child = decode_account_id(parent[0]) - normalized_proportion = u64_normalized_float(proportion) - formatted_parents.append((normalized_proportion, formatted_child)) - return formatted_parents - - return [] - - def get_proxies(self, block: Optional[int] = None) -> dict[str, list[ProxyInfo]]: - """ - Retrieves all proxy relationships from the chain. - - This method queries the Proxy.Proxies storage map across all accounts and returns a dictionary mapping each real - account (delegator) to its list of proxy relationships. - - Parameters: - block: The blockchain block number for the query. If None, queries the latest block. - - Returns: - Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo contains the - delegate address, proxy type, and delay for that proxy relationship. - - Notes: - - This method queries all proxy relationships on the chain, which may be resource-intensive for large - networks. Consider using :meth:`get_proxies_for_real_account` for querying specific accounts. - - See: - """ - block_hash = self.determine_block_hash(block) - query_map = self.substrate.query_map( - module="Proxy", - storage_function="Proxies", - block_hash=block_hash, - ) - - proxies = {} - for record in query_map: - real_account, proxy_list = ProxyInfo.from_query_map_record(record) - proxies[real_account] = proxy_list - return proxies - - def get_proxies_for_real_account( - self, - real_account_ss58: str, - block: Optional[int] = None, - ) -> tuple[list[ProxyInfo], Balance]: - """ - Returns proxy/ies associated with the provided real account. - - This method queries the Proxy.Proxies storage for a specific real account and returns all proxy relationships - where this real account is the delegator. It also returns the deposit amount reserved for these proxies. - - Parameters: - real_account_ss58: SS58 address of the real account (delegator) whose proxies to retrieve. - block: The blockchain block number for the query. - - Returns: - Tuple containing: - - List of ProxyInfo objects representing all proxy relationships for the real account. Each ProxyInfo - contains delegate address, proxy type, and delay. - - Balance object representing the reserved deposit amount for these proxies. This deposit is held as - long as the proxy relationships exist and is returned when proxies are removed. - - Notes: - - If the account has no proxies, returns an empty list and a zero balance. - - See: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query( - module="Proxy", - storage_function="Proxies", - params=[real_account_ss58], - block_hash=block_hash, - ) - return ProxyInfo.from_query(query) - - def get_proxy_announcement( - self, - delegate_account_ss58: str, - block: Optional[int] = None, - ) -> list[ProxyAnnouncementInfo]: - """ - Retrieves proxy announcements for a specific delegate account. - - This method queries the Proxy.Announcements storage for announcements made by the given delegate proxy account. - Announcements allow a proxy to declare its intention to execute a call on behalf of a real account after a delay - period. - - Parameters: - delegate_account_ss58: SS58 address of the delegate proxy account whose announcements to retrieve. - block: The blockchain block number for the query. If None, queries the latest block. - - Returns: - List of ProxyAnnouncementInfo objects. Each object contains the real account address, call hash, and block - height at which the announcement was made. - - Notes: - - If the delegate has no announcements, returns an empty list. - - See: - """ - block_hash = self.determine_block_hash(block) - query = self.substrate.query( - module="Proxy", - storage_function="Announcements", - params=[delegate_account_ss58], - block_hash=block_hash, - ) - return ProxyAnnouncementInfo.from_dict(query.value[0]) - - def get_proxy_announcements( - self, - block: Optional[int] = None, - ) -> dict[str, list[ProxyAnnouncementInfo]]: - """ - Retrieves all proxy announcements from the chain. - - This method queries the Proxy.Announcements storage map across all delegate accounts and returns a dictionary - mapping each delegate to its list of pending announcements. - - Parameters: - block: The blockchain block number for the query. If None, queries the latest block. - - Returns: - Dictionary mapping delegate account SS58 addresses to lists of ProxyAnnouncementInfo objects. - Each ProxyAnnouncementInfo contains the real account address, call hash, and block height. - - Notes: - - This method queries all announcements on the chain, which may be resource-intensive for large networks. - Consider using :meth:`get_proxy_announcement` for querying specific delegates. - - See: - """ - block_hash = self.determine_block_hash(block) - query_map = self.substrate.query_map( - module="Proxy", - storage_function="Announcements", - block_hash=block_hash, - ) - announcements = {} - for record in query_map: - delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) - announcements[delegate] = proxy_list - return announcements - - def get_proxy_constants( - self, - constants: Optional[list[str]] = None, - as_dict: bool = False, - block: Optional[int] = None, - ) -> Union["ProxyConstants", dict]: - """ - Fetches runtime configuration constants from the `Proxy` pallet. - - This method retrieves on-chain configuration constants that define deposit requirements, proxy limits, and - announcement constraints for the Proxy pallet. These constants govern how proxy accounts operate within the - Subtensor network. - - Parameters: - constants: Optional list of specific constant names to fetch. If omitted, all constants defined in - `ProxyConstants.constants_names()` are queried. Valid constant names include: "AnnouncementDepositBase", - "AnnouncementDepositFactor", "MaxProxies", "MaxPending", "ProxyDepositBase", "ProxyDepositFactor". - as_dict: If True, returns the constants as a dictionary instead of a `ProxyConstants` object. - block: The blockchain block number for the query. If None, queries the latest block. - - Returns: - If `as_dict` is False: ProxyConstants object containing all requested constants. - If `as_dict` is True: Dictionary mapping constant names to their values (Balance objects for deposit - constants, integers for limit constants). - - Notes: - - All Balance amounts are returned in RAO. Constants reflect the current chain configuration at the specified - block. - - See: - """ - result = {} - const_names = constants or ProxyConstants.constants_names() - - for const_name in const_names: - query = self.query_constant( - module_name="Proxy", - constant_name=const_name, - block=block, - ) - - if query is not None: - result[const_name] = query.value - - proxy_constants = ProxyConstants.from_dict(result) - - return proxy_constants.to_dict() if as_dict else proxy_constants - - def get_revealed_commitment( - self, - netuid: int, - uid: int, - block: Optional[int] = None, - ) -> Optional[tuple[tuple[int, str], ...]]: - """Returns uid related revealed commitment for a given netuid. - - Parameters: - netuid: The unique identifier of the subnetwork. - uid: The neuron uid to retrieve the commitment from. - block: The block number to retrieve the commitment from. If `None`, queries the current chain head. - - Returns: - A tuple of reveal block and commitment message. - - Example: - - # sample return value - - ( (12, "Alice message 1"), (152, "Alice message 2") ) - - ( (12, "Bob message 1"), (147, "Bob message 2") ) - - Notes: - - - """ - try: - meta_info = self.get_metagraph_info(netuid, block=block) - if meta_info: - hotkey_ss58 = meta_info.hotkeys[uid] - else: - raise ValueError(f"Subnet with netuid {netuid} does not exist.") - except IndexError: - raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") - - return self.get_revealed_commitment_by_hotkey( - netuid=netuid, hotkey_ss58=hotkey_ss58, block=block - ) - - def get_revealed_commitment_by_hotkey( - self, - netuid: int, - hotkey_ss58: str, - block: Optional[int] = None, - ) -> Optional[tuple[tuple[int, str], ...]]: - # TODO: Clarify return ordering and units; add Examples - """Retrieves hotkey related revealed commitment for a given subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - hotkey_ss58: The ss58 address of the committee member. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A tuple of reveal block and commitment message. - - Notes: - - - """ - if not is_valid_ss58_address(address=hotkey_ss58): - raise ValueError(f"Invalid ss58 address {hotkey_ss58} provided.") - - query = self.query_module( - module="Commitments", - name="RevealedCommitments", - params=[netuid, hotkey_ss58], - block=block, - ) - if query is None: - return None - return tuple(decode_revealed_commitment(pair) for pair in query) - - def get_root_claim_type( - self, - coldkey_ss58: str, - block: Optional[int] = None, - ) -> Union[str, dict]: - """Return the configured root claim type for a given coldkey. - - The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they - are claimed: - - - `Swap` (default): Alpha dividends are swapped to TAO at claim time and restaked on the root subnet. - - `Keep`: Alpha dividends remain as Alpha on the originating subnets. - - Parameters: - coldkey_ss58: The SS58 address of the coldkey whose root claim preference to query. - block: The block number to query. Do not specify if using `block_hash` or `reuse_block`. - - Returns: - - The root claim type as a string, either `Swap` or `Keep`, - or dict for "KeepSubnets" in format {"KeepSubnets": {"subnets": [1, 2, 3]}}. - - Notes: - - The claim type applies to both automatic and manual root claims; it does not affect the original TAO stake - on subnet 0, only how Alpha dividends are treated. - - See: - - See also: - """ - query = self.substrate.query( - module="SubtensorModule", - storage_function="RootClaimType", - params=[coldkey_ss58], - block_hash=self.determine_block_hash(block), - ) - # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} - variant_name = next(iter(query.keys())) - variant_value = query[variant_name] - - # For simple variants (Swap, Keep), value is empty tuple, return string - if not variant_value or variant_value == (): - return variant_name - - # For KeepSubnets, value contains the data, return full dict structure - if isinstance(variant_value, dict) and "subnets" in variant_value: - subnets_raw = variant_value["subnets"] - subnets = list(subnets_raw[0]) - - return {variant_name: {"subnets": subnets}} - - return {variant_name: variant_value} - - def get_root_alpha_dividends_per_subnet( - self, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """Retrieves the root alpha dividends per subnet for a given hotkey. - - This storage tracks the root alpha dividends that a hotkey has received on a specific subnet. - It is updated during block emission distribution when root alpha is distributed to validators. - - Parameters: - hotkey_ss58: The ss58 address of the root validator hotkey. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. - """ - query = self.substrate.query( - module="SubtensorModule", - storage_function="RootAlphaDividendsPerSubnet", - params=[netuid, hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - return Balance.from_rao(query.value).set_unit(netuid=netuid) - - def get_root_claimable_rate( - self, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> float: - """Return the fraction of root stake currently claimable on a subnet. - - This method returns a normalized rate representing how much Alpha dividends are currently claimable on the given - subnet relative to the validator's root stake. It is primarily a low-level helper; most users should call - :meth:`get_root_claimable_stake` instead to obtain a Balance. - - Parameters: - hotkey_ss58: The SS58 address of the root validator hotkey. - netuid: The unique identifier of the subnet whose claimable rate to compute. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - A float representing the claimable rate for this subnet (approximately in the range `[0.0, 1.0]`). A value - of 0.0 means there are currently no claimable Alpha dividends on the subnet. - - Notes: - - Use :meth:`get_root_claimable_stake` to retrieve the actual claimable amount as a `Balance` object. - - See: - """ - all_rates = self.get_root_claimable_all_rates( - hotkey_ss58=hotkey_ss58, - block=block, - ) - return all_rates.get(netuid, 0.0) - - def get_root_claimable_all_rates( - self, - hotkey_ss58: str, - block: Optional[int] = None, - ) -> dict[int, float]: - """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. - - Parameters: - hotkey_ss58: The SS58 address of the root validator hotkey. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - Dictionary mapping `netuid` to a float claimable rate (approximately in the range `[0.0, 1.0]`) for that - subnet. Missing entries imply no claimable Alpha dividends for that subnet. - - Notes: - - See: - """ - query = self.substrate.query( - module="SubtensorModule", - storage_function="RootClaimable", - params=[hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - bits_list = next(iter(query.value)) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} - - def get_root_claimable_stake( - self, - coldkey_ss58: str, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """Return the currently claimable Alpha staking dividends for a coldkey from a root validator on a subnet. - - Parameters: - coldkey_ss58: The SS58 address of the delegator's coldkey. - hotkey_ss58: The SS58 address of the root validator hotkey. - netuid: The subnet ID where Alpha dividends will be claimed. - block: The block number to query. If `None`, queries the current chain head. - - Returns: - `Balance` representing the Alpha stake currently available to claim on the specified subnet (unit is the - subnet's Alpha token). - - Notes: - - After a successful manual or automatic claim, this value typically drops to zero for that subnet until new - dividends accumulate. - - The underlying TAO stake on the Root Subnet remains unaffected; only Alpha dividends are moved or swapped - according to the configured root claim type. - - See: - - See also: - """ - root_stake = self.get_stake( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=hotkey_ss58, - netuid=0, # root netuid - block=block, - ) - root_claimable_rate = self.get_root_claimable_rate( - hotkey_ss58=hotkey_ss58, - netuid=netuid, - block=block, - ) - root_claimable_stake = (root_claimable_rate * root_stake).set_unit( - netuid=netuid - ) - root_claimed = self.get_root_claimed( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=hotkey_ss58, - block=block, - netuid=netuid, - ) - return max( - root_claimable_stake - root_claimed, Balance(0).set_unit(netuid=netuid) - ) - - def get_root_claimed( - self, - coldkey_ss58: str, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """Return the total Alpha dividends already claimed for a coldkey from a root validator on a subnet. - - Parameters: - coldkey_ss58: The SS58 address of the delegator's coldkey. - hotkey_ss58: The SS58 address of the root validator hotkey. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - `Balance` representing the cumulative Alpha stake that has already been claimed from the root validator on - the specified subnet. - - Notes: - - See: - """ - query = self.substrate.query( - module="SubtensorModule", - storage_function="RootClaimed", - params=[netuid, hotkey_ss58, coldkey_ss58], - block_hash=self.determine_block_hash(block), - ) - return Balance.from_rao(query.value).set_unit(netuid=netuid) - - def get_stake( - self, - coldkey_ss58: str, - hotkey_ss58: str, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """ - Returns the amount of Alpha staked by a specific coldkey to a specific hotkey within a given subnet. - This function retrieves the delegated stake balance, referred to as the 'Alpha' value. - - Parameters: - coldkey_ss58: The SS58 address of the coldkey that delegated the stake. This address owns the stake. - hotkey_ss58: The ss58 address of the hotkey which the stake is on. - netuid: The unique identifier of the subnet to query. - block: The specific block number at which to retrieve the stake information. - - Returns: - An object representing the amount of Alpha (TAO ONLY if the subnet's netuid is 0) currently staked from the - specified coldkey to the specified hotkey within the given subnet. - """ - alpha_shares_query = self.query_module( - module="SubtensorModule", - name="Alpha", - block=block, - params=[hotkey_ss58, coldkey_ss58, netuid], - ) - alpha_shares = cast(FixedPoint, alpha_shares_query) - - hotkey_alpha_obj: ScaleObj = self.query_module( - module="SubtensorModule", - name="TotalHotkeyAlpha", - block=block, - params=[hotkey_ss58, netuid], - ) - hotkey_alpha = hotkey_alpha_obj.value - - hotkey_shares_query = self.query_module( - module="SubtensorModule", - name="TotalHotkeyShares", - block=block, - params=[hotkey_ss58, netuid], - ) - hotkey_shares = cast(FixedPoint, hotkey_shares_query) - - alpha_shares_as_float = fixed_to_float(alpha_shares) - hotkey_shares_as_float = fixed_to_float(hotkey_shares) - - if hotkey_shares_as_float == 0: - return Balance.from_rao(0).set_unit(netuid=netuid) - - stake = alpha_shares_as_float / hotkey_shares_as_float * hotkey_alpha - - return Balance.from_rao(int(stake)).set_unit(netuid=netuid) - - def get_stake_for_coldkey_and_hotkey( - self, - coldkey_ss58: str, - hotkey_ss58: str, - netuids: Optional[UIDs] = None, - block: Optional[int] = None, - ) -> dict[int, StakeInfo]: - """ - Retrieves all coldkey-hotkey pairing stake across specified (or all) subnets - - Parameters: - coldkey_ss58: The SS58 address of the coldkey. - hotkey_ss58: The SS58 address of the hotkey. - netuids: The subnet IDs to query for. Set to `None` for all subnets. - block: The block number at which to query the stake information. - - Returns: - A netuid to StakeInfo mapping of all stakes across all subnets. - """ - if netuids is None: - all_netuids = self.get_all_subnets_netuid(block=block) - else: - all_netuids = netuids - results = [ - self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_hotkey_coldkey_netuid", - params=[hotkey_ss58, coldkey_ss58, netuid], - block=block, - ) - for netuid in all_netuids - ] - return { - netuid: StakeInfo.from_dict(result) - for (netuid, result) in zip(all_netuids, results) - } - - def get_stake_info_for_coldkey( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> list["StakeInfo"]: - """ - Retrieves the stake information for a given coldkey. - - Parameters: - coldkey_ss58: The SS58 address of the coldkey. - block: The block number at which to query the stake information. - - Returns: - List of StakeInfo objects. - """ - result = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkey", - params=[coldkey_ss58], - block=block, - ) - - if result is None: - return [] - return StakeInfo.list_from_dicts(result) - - def get_stake_info_for_coldkeys( - self, coldkey_ss58s: list[str], block: Optional[int] = None - ) -> dict[str, list["StakeInfo"]]: - """ - Retrieves the stake information for multiple coldkeys. - - Parameters: - coldkey_ss58s: A list of SS58 addresses of the coldkeys to query. - block: The block number at which to query the stake information. - - Returns: - The dictionary mapping coldkey addresses to a list of StakeInfo objects. - """ - query = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkeys", - params=[coldkey_ss58s], - block=block, - ) - - if query is None: - return {} - - return { - decode_account_id(ck): StakeInfo.list_from_dicts(st_info) - for ck, st_info in query - } - - def get_stake_for_hotkey( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Balance: - """ - Retrieves the stake information for a given hotkey. - - Parameters: - hotkey_ss58: The SS58 address of the hotkey. - netuid: The subnet ID to query for. - block: The block number at which to query the stake information. - """ - hotkey_alpha_query = self.query_subtensor( - name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block - ) - hotkey_alpha = cast(ScaleObj, hotkey_alpha_query) - balance = Balance.from_rao(hotkey_alpha.value) - balance.set_unit(netuid=netuid) - return balance - - get_hotkey_stake = get_stake_for_hotkey - - def get_stake_add_fee( - self, - amount: Balance, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """ - Calculates the fee for adding new stake to a hotkey. - - Parameters: - amount: Amount of stake to add in TAO - netuid: Netuid of subnet - block: Block number at which to perform the calculation - - Returns: - The calculated stake fee as a Balance object in TAO. - """ - check_balance_amount(amount) - sim_swap_result = self.sim_swap( - origin_netuid=0, destination_netuid=netuid, amount=amount, block=block - ) - return sim_swap_result.tao_fee - - def get_stake_movement_fee( - self, - origin_netuid: int, - destination_netuid: int, - amount: Balance, - block: Optional[int] = None, - ) -> Balance: - """ - Calculates the fee for moving stake between hotkeys/subnets/coldkeys. - - Parameters: - origin_netuid: Netuid of source subnet. - destination_netuid: Netuid of the destination subnet. - amount: Amount of stake to move. - block: The block number for which the children are to be retrieved. - - Returns: - The calculated stake fee as a Balance object - """ - check_balance_amount(amount) - sim_swap_result = self.sim_swap( - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, - amount=amount, - block=block, - ) - return sim_swap_result.tao_fee - - def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[float]: - """ - Retrieves the stake weight for all hotkeys in a given subnet. - - Parameters: - netuid: Netuid of subnet. - block: Block number at which to perform the calculation. - - Returns: - A list of stake weights for all hotkeys in the specified subnet. - """ - block_hash = self.determine_block_hash(block=block) - result = self.substrate.query( - module="SubtensorModule", - storage_function="StakeWeight", - params=[netuid], - block_hash=block_hash, - ) - return [u16_normalized_float(w) for w in result] - - def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: - """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the - amount of Tao that needs to be locked or burned to establish a new subnet. - - Parameters: - block: The blockchain block number for the query. - - Returns: - int: The burn cost for subnet registration. - - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the - proliferation of subnets and ensuring their commitment to the network's long-term viability. - """ - lock_cost = self.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=block, - ) - - if lock_cost is not None: - return Balance.from_rao(lock_cost) - else: - return lock_cost - - def get_subnet_hyperparameters( - self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[list, "SubnetHyperparameters"]]: - """ - Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define - the operational settings and rules governing the subnet's behavior. - - Parameters: - netuid: The network UID of the subnet to query. - block: The blockchain block number for the query. - - Returns: - The subnet's hyperparameters, or `None` if not available. - - Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how - they interact with the network's consensus and incentive mechanisms. - """ - result = self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams_v2", - params=[netuid], - block=block, - ) - - if not result: - return None - - return SubnetHyperparameters.from_dict(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. - - Parameters: - 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_subnet_owner_hotkey( - self, netuid: int, block: Optional[int] = None - ) -> Optional[str]: - """ - Retrieves the hotkey of the subnet owner for a given network UID. - - This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its - netuid. If no data is found or the query fails, the function returns `None`. - - Parameters: - netuid: The network UID of the subnet to fetch the owner's hotkey for. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The hotkey of the subnet owner if available; `None` otherwise. - """ - return self.query_subtensor( - name="SubnetOwnerHotkey", params=[netuid], block=block - ) - - def get_subnet_price( - self, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """Gets the current Alpha price in TAO for the specified subnet. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The current Alpha price in TAO units for the specified subnet. - - Notes: - Subnet 0 (root network) always returns 1 TAO since it uses TAO directly rather than Alpha. - """ - # SN0 price is always 1 TAO - if netuid == 0: - return Balance.from_tao(1) - - block_hash = self.determine_block_hash(block=block) - price_rao = self.substrate.runtime_call( - api="SwapRuntimeApi", - method="current_alpha_price", - params=[netuid], - block_hash=block_hash, - ).value - return Balance.from_rao(price_rao) - - def get_subnet_prices( - self, - block: Optional[int] = None, - ) -> dict[int, Balance]: - """Gets the current Alpha price in TAO for all subnets. - - Parameters: - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - A dictionary mapping subnet unique ID (netuid) to the current Alpha price in TAO units. - - Notes: - Subnet 0 (root network) always has a price of 1 TAO since it uses TAO directly rather than Alpha. - """ - 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_subnet_reveal_period_epochs( - self, netuid: int, block: Optional[int] = None - ) -> int: - """Retrieves the SubnetRevealPeriodEpochs hyperparameter for a specified subnet. - - This hyperparameter determines the number of epochs that must pass before a committed weight can be revealed - in the commit-reveal mechanism. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The number of epochs in the reveal period for the subnet. - - Notes: - - - - """ - return cast( - int, - self.get_hyperparameter( - param_name="RevealPeriodEpochs", block=block, netuid=netuid - ), - ) - - def get_subnet_validator_permits( - self, netuid: int, block: Optional[int] = None - ) -> Optional[list[bool]]: - """ - Retrieves the list of validator permits for a given subnet as boolean values. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - A list of boolean values representing validator permits, or None if not available. - """ - query = self.query_subtensor( - name="ValidatorPermit", - params=[netuid], - block=block, - ) - return query.value if query is not None and hasattr(query, "value") else query - - def get_timelocked_weight_commits( - self, - netuid: int, - mechid: int = 0, - block: Optional[int] = None, - ) -> list[tuple[str, int, str, int]]: - """Retrieves CRv4 (Commit-Reveal version 4) weight commit information for a specific subnet. - - This method retrieves timelocked weight commitments made by validators using the commit-reveal mechanism. - The raw byte/vector encoding from the chain is automatically parsed and converted into a structured format - via `WeightCommitInfo`. - - Parameters: - netuid: The unique identifier of the subnet. - mechid: Subnet mechanism identifier (default 0 for primary mechanism). - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - A list of commit details, where each item is a tuple containing: - - - ss58_address: The SS58 address of the committer. - - commit_block: The block number when the commitment was made. - - commit_message: The commit message (encoded commitment data). - - reveal_round: The drand round when the commitment can be revealed. - - Notes: - The list may be empty if there are no commits found. - - - """ - storage_index = get_mechid_storage_index(netuid, mechid) - result = self.substrate.query_map( - module="SubtensorModule", - storage_function="TimelockedWeightCommits", - params=[storage_index], - block_hash=self.determine_block_hash(block=block), - ) - - commits = result.records[0][1] if result.records else [] - return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] - - def get_timestamp(self, block: Optional[int] = None) -> datetime: - """ - Retrieves the datetime timestamp for a given block - - Parameters: - block: The blockchain block number for the query. - - Returns: - datetime object for the timestamp of the block - """ - unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value - return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) - - def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: - """Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - - Parameters: - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The total number of subnets in the network. - - """ - result = self.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", - params=[], - block_hash=self.determine_block_hash(block), - ) - return getattr(result, "value", None) - - def get_transfer_fee( - self, - wallet: "Wallet", - destination_ss58: str, - amount: Optional[Balance], - keep_alive: bool = True, - ) -> Balance: - """ - Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This - function simulates the transfer to estimate the associated cost, taking into account the current network - conditions and transaction complexity. - - Parameters: - wallet: The wallet from which the transfer is initiated. - destination_ss58: The `SS58` address of the destination account. - amount: The amount of tokens to be transferred, specified as a Balance object, or in Tao or Rao units. - keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential - deposit) or not. - - Returns: - bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance - object. - - Notes: - - - """ - check_balance_amount(amount) - call_params: dict[str, Union[int, str, bool]] - call_function, call_params = get_transfer_fn_params( - amount, destination_ss58, keep_alive - ) - - call = self.compose_call( - call_module="Balances", - call_function=call_function, - call_params=call_params, - ) - - try: - payment_info = self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except Exception as e: - logging.error(f":cross_mark: [red]Failed to get payment info: [/red]{e}") - payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao - - return Balance.from_rao(payment_info["partial_fee"]) - - def get_unstake_fee( - self, - netuid: int, - amount: Balance, - block: Optional[int] = None, - ) -> Balance: - """Calculates the fee for unstaking from a hotkey. - - Parameters: - netuid: The unique identifier of the subnet. - amount: Amount of stake to unstake in TAO. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - The calculated stake fee as a Balance object in Alpha. - - Notes: - - - """ - check_balance_amount(amount) - sim_swap_result = self.sim_swap( - origin_netuid=netuid, - destination_netuid=0, - amount=amount, - block=block, - ) - return sim_swap_result.alpha_fee.set_unit(netuid=netuid) - - def get_vote_data( - self, proposal_hash: str, block: Optional[int] = None - ) -> Optional["ProposalVoteData"]: - # TODO: is this all deprecated? Didn't subtensor senate stuff get removed? - """ - Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information - about how senate members have voted on the proposal. - - Parameters: - proposal_hash: The hash of the proposal for which voting data is requested. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - An object containing the proposal's voting data, or `None` if not found. - - This function is important for tracking and understanding the decision-making processes within the Bittensor - network, particularly how proposals are received and acted upon by the governing body. - """ - vote_data: dict[str, Any] = self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=self.determine_block_hash(block), - ) - - if vote_data is None: - return None - - return ProposalVoteData.from_dict(vote_data) - - def get_uid_for_hotkey_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. - - Parameters: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - The UID of the neuron if it is registered on the subnet, `None` otherwise. - - The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and - governance activities on a particular subnet. - """ - result = self.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], - block_hash=self.determine_block_hash(block), - ) - return getattr(result, "value", result) - - def filter_netuids_by_registered_hotkeys( - self, - all_netuids: Iterable[int], - filter_for_netuids: Iterable[int], - all_hotkeys: Iterable["Wallet"], - block: Optional[int], - ) -> list[int]: - """ - Filters netuids by combining netuids from all_netuids and netuids with registered hotkeys. - - If filter_for_netuids is empty/None: - Returns all netuids where hotkeys from all_hotkeys are registered. - - If filter_for_netuids is provided: - Returns the union of: - - Netuids from all_netuids that are in filter_for_netuids, AND - - Netuids with registered hotkeys that are in filter_for_netuids - - This allows you to get netuids that are either in your specified list (all_netuids) or have registered hotkeys, - as long as they match filter_for_netuids. - - Parameters: - all_netuids (Iterable[int]): A list of netuids to consider for filtering. - filter_for_netuids (Iterable[int]): A subset of netuids to restrict the result to. If None/empty, returns - all netuids with registered hotkeys. - all_hotkeys (Iterable[Wallet]): Hotkeys to check for registration. - block (Optional[int]): The blockchain block number for the query. - - Returns: - The filtered list of netuids (union of filtered all_netuids and registered hotkeys). - """ - self._get_block_hash(block) # just used to cache the block hash - netuids_with_registered_hotkeys = [ - item - for sublist in [ - self.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, - block=block, - ) - for wallet in all_hotkeys - ] - for item in sublist - ] - - if not filter_for_netuids: - all_netuids = netuids_with_registered_hotkeys - - else: - filtered_netuids = [ - netuid for netuid in all_netuids if netuid in filter_for_netuids - ] - - registered_hotkeys_filtered = [ - netuid - for netuid in netuids_with_registered_hotkeys - if netuid in filter_for_netuids - ] - - # Combine both filtered lists - all_netuids = filtered_netuids + registered_hotkeys_filtered - - return list(set(all_netuids)) - - def immunity_period( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during - which new neurons are protected from certain network penalties or restrictions. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, `None` otherwise. - - The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants - have a grace period to establish themselves and contribute to the network without facing immediate punitive - actions. - """ - call = self.get_hyperparameter( - param_name="ImmunityPeriod", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def is_in_admin_freeze_window( - self, - netuid: int, - block: Optional[int] = None, - ) -> bool: - """ - Returns True if the current block is within the terminal freeze window of the tempo - for the given subnet. During this window, admin ops are prohibited to avoid interference - with validator weight submissions. - - Parameters: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - bool: True if in freeze window, else False. - """ - # SN0 doesn't have admin_freeze_window - if netuid == 0: - return False - - next_epoch_start_block = self.get_next_epoch_start_block( - netuid=netuid, block=block - ) - - if next_epoch_start_block is not None: - remaining = next_epoch_start_block - self.block - window = self.get_admin_freeze_window(block=block) - return remaining < window - return False - - def is_fast_blocks(self) -> bool: - """Checks if the node is running with fast blocks enabled. - - Fast blocks have a block time of 10 seconds, compared to the standard 12-second block time. This affects - transaction timing and network synchronization. - - Returns: - `True` if fast blocks are enabled (10-second block time), `False` otherwise (12-second block time). - - Notes: - - - - """ - return self.query_constant("SubtensorModule", "DurationOfStartCall") == 10 - - def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if - the neuron associated with the hotkey is part of the network's delegation system. - - Parameters: - hotkey_ss58: The SS58 address of the neuron's hotkey. - block: The blockchain block number for the query. - - Returns: - `True` if the hotkey is a delegate, `False` otherwise. - - Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in - consensus and governance processes. - """ - delegates = self.get_delegates(block) - return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] - - def is_hotkey_registered( - self, - hotkey_ss58: str, - netuid: Optional[int] = None, - block: Optional[int] = None, - ) -> bool: - """ - Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across - any subnet or specifically on a specified subnet. This function checks the registration status of a neuron - identified by its hotkey, which is crucial for validating its participation and activities within the network. - - Parameters: - hotkey_ss58: The SS58 address of the neuron's hotkey. - netuid: The unique identifier of the subnet to check the registration. If `None`, the registration is - checked across all subnets. - block: The blockchain block number at which to perform the query. - - Returns: - `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), - `False` otherwise. - - This function is important for verifying the active status of neurons in the Bittensor network. It aids in - understanding whether a neuron is eligible to participate in network processes such as consensus, validation, - and incentive distribution based on its registration status. - """ - if netuid is None: - return self.is_hotkey_registered_any(hotkey_ss58, block) - else: - return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - - def is_hotkey_registered_any( - self, - hotkey_ss58: str, - block: Optional[int] = None, - ) -> bool: - """ - Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. - - Parameters: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: The blockchain block number for the query. - - Returns: - `True` if the hotkey is registered on any subnet, False otherwise. - """ - hotkeys = self.get_netuids_for_hotkey(hotkey_ss58, block) - return len(hotkeys) > 0 - - def is_hotkey_registered_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> bool: - """Checks if the hotkey is registered on a given subnet. - - Parameters: - hotkey_ss58: The SS58 address of the hotkey to check. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - `True` if the hotkey is registered on the specified subnet, `False` otherwise. - - Notes: - - - - """ - return ( - self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block) - is not None - ) - - def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: - """Verifies if a subnet with the provided netuid is active. - - A subnet is considered active if the `start_call` extrinsic has been executed. A newly registered subnet - may exist but not be active until the subnet owner calls `start_call` to begin emissions. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. If `None`, queries the current chain head. - - Returns: - `True` if the subnet is active (emissions have started), `False` otherwise. - - Notes: - - - - """ - query = self.query_subtensor( - name="FirstEmissionBlockNumber", - block=block, - params=[netuid], - ) - return True if query and query.value > 0 else False - - def last_drand_round(self) -> Optional[int]: - """Retrieves the last drand round emitted in Bittensor. - - Drand (distributed randomness) rounds are used to determine when committed weights can be revealed in the - commit-reveal mechanism. This method returns the most recent drand round number, which corresponds to the - timing for weight reveals. - - Returns: - The latest drand round number emitted in Bittensor, or `None` if no round has been stored. - - Notes: - - - - """ - result = self.substrate.query( - module="Drand", storage_function="LastStoredRound" - ) - return getattr(result, "value", None) - - def max_weight_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """Returns the MaxWeightsLimit hyperparameter for a subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - The stored maximum weight limit as a normalized float in [0, 1], or `None` if the subnetwork - does not exist. Note: this value is not actually enforced - the weight validation code uses - a hardcoded u16::MAX instead. - - Notes: - - This hyperparameter is now a constant rather than a settable variable. - - - """ - call = self.get_hyperparameter( - param_name="MaxWeightsLimit", netuid=netuid, block=block - ) - return None if call is None else u16_normalized_float(int(call)) - - def metagraph( - self, - netuid: int, - mechid: int = 0, - lite: bool = True, - block: Optional[int] = None, - ) -> "Metagraph": - """ - Returns a synced metagraph for a specified subnet within the Bittensor network. - The metagraph represents the network's structure, including neuron connections and interactions. - - Parameters: - netuid: The network UID of the subnet to query. - mechid: Subnet mechanism identifier. - lite: If `True`, returns a metagraph using a lightweight sync (no weights, no bonds). - block: Block number for synchronization, or `None` for the latest block. - - Returns: - The metagraph representing the subnet's structure and neuron relationships. - - The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's - decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. - """ - metagraph = Metagraph( - netuid=netuid, - mechid=mechid, - network=self.chain_endpoint, - lite=lite, - sync=False, - subtensor=self, - ) - metagraph.sync(block=block, lite=lite, subtensor=self) - - return metagraph - - def min_allowed_weights( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """Returns the MinAllowedWeights hyperparameter for a subnet. - - This hyperparameter sets the minimum length of the weights vector that a validator must submit. - It checks `weights.len() >= MinAllowedWeights`. For example, a validator could submit `[1000, 0, 0, 0]` - to satisfy `MinAllowedWeights=4`, but this would fail if `MinAllowedWeights` were set to 5. - This ensures validators distribute attention across the subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - The minimum number of required weight connections, or `None` if the subnetwork does not - exist or the parameter is not found. - - Notes: - - - """ - call = self.get_hyperparameter( - param_name="MinAllowedWeights", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def neuron_for_uid( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> "NeuronInfo": - """ - Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a - specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's - attributes, including its stake, rank, and operational status. - - Parameters: - uid: The unique identifier of the neuron. - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - Detailed information about the neuron if found, a null neuron otherwise - - This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, - offering insights into their roles in the network's consensus and validation mechanisms. - """ - if uid is None: - return NeuronInfo.get_null_neuron() - - result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neuron", - params=[netuid, uid], - block=block, - ) - - if not result: - return NeuronInfo.get_null_neuron() - - return NeuronInfo.from_dict(result) - - def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: - """ - Retrieves a list of all neurons within a specified subnet of the Bittensor network. - This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and - network interactions. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. - - Understanding the distribution and status of neurons within a subnet is key to comprehending the network's - decentralized structure and the dynamics of its consensus and governance processes. - """ - result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons", - params=[netuid], - block=block, - ) - - if not result: - return [] - - return NeuronInfo.list_from_dicts(result) - - def neurons_lite( - self, netuid: int, block: Optional[int] = None - ) -> list["NeuronInfoLite"]: - """ - Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. - This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network - participation. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - A list of simplified neuron information for the subnet. - - This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis - of the network's decentralized structure and neuron dynamics. - """ - result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[netuid], - block=block, - ) - - if not result: - return [] - - return NeuronInfoLite.list_from_dicts(result) - - def query_identity( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional[ChainIdentity]: - """ - Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves - detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized - identity and governance system. - - Parameters: - coldkey_ss58: Coldkey used to query the neuron's identity (technically the neuron's coldkey SS58 address). - block: The blockchain block number for the query. - - Returns: - An object containing the identity information of the neuron if found, `None` otherwise. - - The identity information can include various attributes such as the neuron's stake, rank, and other - network-specific details, providing insights into the neuron's role and status within the Bittensor network. - - Note: - See the `Bittensor CLI documentation `_ for supported identity - parameters. - """ - identity_info = cast( - dict, - self.substrate.query( - module="SubtensorModule", - storage_function="IdentitiesV2", - params=[coldkey_ss58], - block_hash=self.determine_block_hash(block), - ), - ) - - if not identity_info: - return None - - try: - return ChainIdentity.from_dict( - decode_hex_identity_dict(identity_info), - ) - except TypeError: - return None - - def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: - """Retrieves the 'Burn' hyperparameter for a specified subnet. - - The 'Burn' parameter represents the amount of TAO that is recycled when registering a neuron - on this subnet. Recycled tokens are removed from circulation but can be re-emitted, unlike - burned tokens which are permanently removed. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - The amount of TAO recycled per neuron registration, or `None` if the subnet does not exist. - - Notes: - - - """ - call = self.get_hyperparameter(param_name="Burn", netuid=netuid, block=block) - return None if call is None else Balance.from_rao(int(call)) - - def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicInfo]: - """ - Retrieves the subnet information for a single subnet in the network. - - Parameters: - netuid: The unique identifier of the subnet. - block: The block number to query the subnet information from. - - Returns: - A DynamicInfo object, containing detailed information about a subnet. - """ - block_hash = self.determine_block_hash(block=block) - - query = self.substrate.runtime_call( - api="SubnetInfoRuntimeApi", - method="get_dynamic_info", - params=[netuid], - block_hash=block_hash, - ) - - if isinstance(decoded := query.decode(), dict): - try: - price = self.get_subnet_price(netuid=netuid, block=block) - except (SubstrateRequestException, ValueError): - price = None - return DynamicInfo.from_dict({**decoded, "price": price}) - return None - - def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: - """ - Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - - Parameters: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. - - Returns: - `True` if the subnet exists, `False` otherwise. - - This function is critical for verifying the presence of specific subnets in the network, enabling a deeper - understanding of the network's structure and composition. - """ - result = self.substrate.query( - module="SubtensorModule", - storage_function="NetworksAdded", - params=[netuid], - block_hash=self.determine_block_hash(block), - ) - return getattr(result, "value", False) - - def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """Returns the current number of registered neurons (UIDs) in a subnet. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - The current number of registered neurons in the subnet, or `None` if the subnetwork does not exist. - - """ - call = self.get_hyperparameter( - param_name="SubnetworkN", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """Returns the Tempo hyperparameter for a subnet. - - Tempo determines the length of an epoch in blocks. It defines how frequently the subnet's consensus mechanism - runs, calculating emissions and updating rankings. A tempo of 360 blocks equals approximately 72 minutes - (360 blocks × 12 seconds per block). - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - The tempo value in blocks, or `None` if the subnetwork does not exist. - - Notes: - - - - - """ - call = self.get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) - return None if call is None else int(call) - - def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. - This rate limit sets the maximum number of transactions that can be processed within a given time frame. - - Parameters: - block: The blockchain block number for the query. - - Returns: - The transaction rate limit of the network, None if not available. - - The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor - network. It helps in managing network load and preventing congestion, thereby maintaining efficient and - timely transaction processing. - """ - result = self.query_subtensor("TxRateLimit", block=block) - return getattr(result, "value", None) - - def wait_for_block(self, block: Optional[int] = None): - """ - Waits until a specific block is reached on the chain. If no block is specified, - waits for the next block. - - Parameters: - block: The block number to wait for. If None, waits for the next block. - - Returns: - True if the target block was reached, False if timeout occurred. - - Example: - - # Waits for a specific block - - subtensor.wait_for_block(block=1234) - """ - - def handler(block_data: dict): - logging.debug( - f"reached block {block_data['header']['number']}. Waiting for block {target_block}" - ) - if block_data["header"]["number"] >= target_block: - return True - return None - - current_block = self.substrate.get_block() - current_block_hash = current_block.get("header", {}).get("hash") - if block is not None: - target_block = block - else: - target_block = current_block["header"]["number"] + 1 - - self.substrate.get_block_handler( - current_block_hash, header_only=True, subscription_handler=handler - ) - return True - - def weights( - self, - netuid: int, - mechid: int = 0, - block: Optional[int] = None, - ) -> list[tuple[int, list[tuple[int, int]]]]: - """ - Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. - This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust - and value assignment mechanisms. - - Parameters: - netuid: The network UID of the subnet to query. - mechid: Subnet mechanism identifier. - block: Block number for synchronization, or `None` for the latest block. - - Returns: - A list of tuples mapping each neuron's UID to its assigned weights. - - The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, - influencing their influence and reward allocation within the subnet. - """ - storage_index = get_mechid_storage_index(netuid, mechid) - w_map_encoded = self.substrate.query_map( - module="SubtensorModule", - storage_function="Weights", - params=[storage_index], - block_hash=self.determine_block_hash(block), - ) - w_map = [(uid, w.value or []) for uid, w in w_map_encoded] - - return w_map - - def weights_rate_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """Returns the WeightsSetRateLimit hyperparameter for a subnet. - - This hyperparameter limits how many times a validator can set weights per epoch. It prevents validators - from spamming weight updates and ensures stable consensus calculations. Once the limit is reached, validators - must wait until the next epoch to set weights again. - - Parameters: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number for the query. - - Returns: - The maximum number of weight set operations allowed per epoch, or `None` if the subnetwork does not - exist or the parameter is not found. - """ - call = self.get_hyperparameter( - param_name="WeightsSetRateLimit", netuid=netuid, block=block - ) - return None if call is None else int(call) - - # Extrinsics helpers =============================================================================================== - - def validate_extrinsic_params( - self, - call_module: str, - call_function: str, - call_params: dict[str, Any], - block: Optional[int] = None, - ): - """ - Validate and filter extrinsic parameters against on-chain metadata. - - This method checks that the provided parameters match the expected signature of the given extrinsic (module and - function) as defined in the Substrate metadata. It raises explicit errors for missing or invalid parameters and - silently ignores any extra keys not present in the function definition. - - Parameters: - call_module: The pallet name, e.g. "SubtensorModule" or "AdminUtils". - call_function: The extrinsic function name, e.g. "set_weights" or "sudo_set_tempo". - call_params: A dictionary of parameters to validate. - block: Optional block number to query metadata from. If not provided, the latest metadata is used. - - Returns: - A filtered dictionary containing only the parameters that are valid for the specified extrinsic. - - Raises: - ValueError: If the given module or function is not found in the chain metadata. - KeyError: If one or more required parameters are missing. - - Notes: - This method does not compose or submit the extrinsic. It only ensures that `call_params` conforms to the - expected schema derived from on-chain metadata. - """ - block_hash = self.determine_block_hash(block=block) - - func_meta = self.substrate.get_metadata_call_function( - module_name=call_module, - call_function_name=call_function, - block_hash=block_hash, - ) - - if not func_meta: - raise ValueError( - f"Call {call_module}.{call_function} not found in chain metadata." - ) - - # Expected params from metadata - expected_params = func_meta.get_param_info() - provided_params = {} - - # Validate and filter parameters - for param_name in expected_params.keys(): - if param_name not in call_params: - raise KeyError(f"Missing required parameter: '{param_name}'") - provided_params[param_name] = call_params[param_name] - - # Warn about extra params not defined in metadata - extra_params = set(call_params.keys()) - set(expected_params.keys()) - if extra_params: - logging.debug( - f"Ignoring extra parameters for {call_module}.{call_function}: {extra_params}." - ) - return provided_params - - def compose_call( - self, - call_module: str, - call_function: str, - call_params: dict[str, Any], - block: Optional[int] = None, - ) -> "GenericCall": - """ - Dynamically compose a GenericCall using on-chain Substrate metadata after validating the provided parameters. - - Parameters: - call_module: Pallet name (e.g. "SubtensorModule", "AdminUtils"). - call_function: Function name (e.g. "set_weights", "sudo_set_tempo"). - call_params: Dictionary of parameters for the call. - block: Block number for querying metadata. - - Returns: - GenericCall: Composed call object ready for extrinsic submission. - - Notes: - For detailed documentation and examples of composing calls, including the CallBuilder utility, see: - - """ - call_params = self.validate_extrinsic_params( - call_module, call_function, call_params, block - ) - block_hash = self.determine_block_hash(block=block) - logging.debug( - f"Composing GenericCall -> {call_module}.{call_function} " - f"with params: {call_params}." - ) - return self.substrate.compose_call( - call_module=call_module, - call_function=call_function, - call_params=call_params, - block_hash=block_hash, - ) - - def sign_and_send_extrinsic( - self, - call: "GenericCall", - wallet: "Wallet", - sign_with: str = "coldkey", - use_nonce: bool = False, - nonce_key: str = "hotkey", - nonce: Optional[int] = None, - *, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - calling_function: Optional[str] = None, - ) -> ExtrinsicResponse: - """ - Helper method to sign and submit an extrinsic call to chain. - - Parameters: - call: A prepared Call object - wallet: The wallet whose coldkey will be used to sign the extrinsic - sign_with: The wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" - use_nonce: Unique identifier for the transaction related with hot/coldkey. - nonce_key: The type on nonce to use. Options are "hotkey" or "coldkey". - nonce: The nonce to use for the transaction. If not provided, it will be fetched from the chain. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises the relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait until the extrinsic call is included on the chain - wait_for_finalization: Whether to wait until the extrinsic call is finalized on the chain - calling_function: The name of the calling function. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Raises: - SubstrateRequestException: Substrate request exception. - """ - extrinsic_response = ExtrinsicResponse( - extrinsic_function=calling_function - if calling_function - else get_caller_name() - ) - possible_keys = ("coldkey", "hotkey", "coldkeypub") - if sign_with not in possible_keys: - raise AttributeError( - f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" - ) - - signing_keypair = getattr(wallet, sign_with) - extrinsic_data = {"call": call, "keypair": signing_keypair} - - if nonce is not None: - extrinsic_data["nonce"] = nonce - elif use_nonce: - if nonce_key not in possible_keys: - raise AttributeError( - f"'nonce_key' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{nonce_key}'" - ) - next_nonce = self.substrate.get_account_next_index( - getattr(wallet, nonce_key).ss58_address - ) - extrinsic_data["nonce"] = next_nonce - - if period is not None: - extrinsic_data["era"] = {"period": period} - - extrinsic_response.extrinsic = self.substrate.create_signed_extrinsic( - **extrinsic_data - ) - try: - response = self.substrate.submit_extrinsic( - extrinsic=extrinsic_response.extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - extrinsic_response.extrinsic_fee = self.get_extrinsic_fee( - call=call, keypair=signing_keypair - ) - extrinsic_response.message = ( - "Not waiting for finalization or inclusion." - ) - logging.debug(extrinsic_response.message) - return extrinsic_response - - extrinsic_response.extrinsic_receipt = response - - if response.is_success: - extrinsic_response.extrinsic_fee = Balance.from_rao( - response.total_fee_amount - ) - extrinsic_response.message = "Success" - return extrinsic_response - - response_error_message = response.error_message - - if raise_error: - raise ChainError.from_error(response_error_message) - - extrinsic_response.success = False - extrinsic_response.message = format_error_message(response_error_message) - extrinsic_response.error = response_error_message - return extrinsic_response - - except SubstrateRequestException as error: - if raise_error: - raise - - extrinsic_response.success = False - extrinsic_response.message = format_error_message(error) - extrinsic_response.error = error - return extrinsic_response - - def get_extrinsic_fee( - self, - call: "GenericCall", - keypair: "Keypair", - ) -> Balance: - """Gets the extrinsic fee for a given extrinsic call and keypair. - - This method estimates the transaction fee that will be charged for submitting the extrinsic to the - blockchain. The fee is returned in Rao (the smallest unit of TAO, where 1 TAO = 1e9 Rao). - - Parameters: - call: The extrinsic GenericCall object representing the transaction to estimate. - keypair: The keypair associated with the extrinsic (used to determine the account paying the fee). - - Returns: - Balance object representing the extrinsic fee in Rao. - - Example: - - # Estimate fee before sending a transfer - - call = subtensor.compose_call( - - call_module="Balances", - - call_function="transfer", - - call_params={"dest": destination_ss58, "value": amount.rao} - - ) - - fee = subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) - - print(f"Estimated fee: {fee.tao} TAO") - - Notes: - To create the GenericCall object, use the `compose_call` method with proper parameters. - - - - """ - payment_info = self.substrate.get_payment_info(call=call, keypair=keypair) - return Balance.from_rao(amount=payment_info["partial_fee"]) - - # Extrinsics ======================================================================================================= - - def add_stake( - self, - wallet: "Wallet", - netuid: int, - hotkey_ss58: str, - amount: Balance, - safe_staking: bool = False, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Adds stake from the specified wallet to a neuron on a specified subnet. - - Staking is a fundamental process in the Bittensor network that enables neurons to participate actively - and earn incentives. This method transfers TAO from the coldkey to stake on a hotkey in a specific subnet, - converting it to Alpha (subnet-specific token) in the process. - - Parameters: - wallet: The wallet to be used for staking. - netuid: The unique identifier of the subnet to which the neuron belongs. - hotkey_ss58: The `SS58` address of the hotkey account to stake to. - amount: The amount of TAO to stake. - safe_staking: If `True`, enables price safety checks to protect against fluctuating prices. The stake will - only execute if the price change doesn't exceed the rate tolerance. - allow_partial_stake: If `True` and safe_staking is enabled, allows partial staking when the full amount would - exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. - rate_tolerance: The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price - increase. Only used when safe_staking is True. - mev_protection: If `True`, encrypts and submits the staking transaction through the MEV Shield pallet to - protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until - validators decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection - used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - When safe_staking is enabled, it provides protection against price fluctuations during the time between when - stake is submitted and when it is actually processed by the chain. - - - - - Price Protection: - - Rate Limits: - """ - check_balance_amount(amount) - return add_stake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - amount=amount, - safe_staking=safe_staking, - allow_partial_stake=allow_partial_stake, - rate_tolerance=rate_tolerance, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def add_liquidity( - self, - wallet: "Wallet", - netuid: int, - liquidity: Balance, - price_low: Balance, - price_high: Balance, - hotkey_ss58: Optional[str] = None, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Adds liquidity to the specified price range. - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - liquidity: The amount of liquidity to be added. - price_low: The lower bound of the price tick range. In TAO. - price_high: The upper bound of the price tick range. In TAO. - hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` - method to enable/disable user liquidity. - """ - return add_liquidity_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - liquidity=liquidity, - price_low=price_low, - price_high=price_high, - hotkey_ss58=hotkey_ss58, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def add_stake_multiple( - self, - wallet: "Wallet", - netuids: UIDs, - hotkey_ss58s: list[str], - amounts: list[Balance], - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Adds stakes to multiple neurons identified by their hotkey SS58 addresses. - This bulk operation allows for efficient staking across different neurons from a single wallet. - - Parameters: - wallet: The wallet used for staking. - netuids: List of subnet UIDs. - hotkey_ss58s: List of `SS58` addresses of hotkeys to stake to. - amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Price Protection: - - Rate Limits: - """ - return add_stake_multiple_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def add_proxy( - self, - wallet: "Wallet", - delegate_ss58: str, - proxy_type: Union[str, "ProxyType"], - delay: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Adds a proxy relationship. - - This method creates a proxy relationship where the delegate can execute calls on behalf of the real account (the - wallet owner) with restrictions defined by the proxy type and a delay period. A deposit is required and held as - long as the proxy relationship exists. - - Parameters: - wallet: Bittensor wallet object. - delegate_ss58: The SS58 address of the delegate proxy account. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a - string or ProxyType enum value. - delay: The number of blocks before the proxy can be used. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - A deposit is required when adding a proxy. The deposit amount is determined by runtime constants and is - returned when the proxy is removed. Use :meth:`get_proxy_constants` to check current deposit requirements. - - See: - """ - return add_proxy_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - proxy_type=proxy_type, - delay=delay, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def announce_proxy( - self, - wallet: "Wallet", - real_account_ss58: str, - call_hash: str, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Announces a future call that will be executed through a proxy. - - This method allows a proxy account to declare its intention to execute a specific call on behalf of a real - account after a delay period. The real account can review and either approve (via :meth:`proxy_announced`) or reject - (via :meth:`reject_proxy_announcement`) the announcement. - - Parameters: - wallet: Bittensor wallet object (should be the proxy account wallet). - real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. - call_hash: The hash of the call that will be executed in the future. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - A deposit is required when making an announcement. The deposit is returned when the announcement is - executed, rejected, or removed. The announcement can be executed after the delay period has passed. - - See: - """ - return announce_extrinsic( - subtensor=self, - wallet=wallet, - real_account_ss58=real_account_ss58, - call_hash=call_hash, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def burned_register( - self, - wallet: "Wallet", - netuid: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling - TAO tokens, allowing them to be re-mined by performing work on the network. - - Parameters: - wallet: The wallet associated with the neuron to be registered. - netuid: The unique identifier of the subnet. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Rate Limits: - """ - - if netuid == 0: - return root_register_extrinsic( - subtensor=self, - wallet=wallet, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - return burned_register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def claim_root( - self, - wallet: "Wallet", - netuids: "UIDs", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ): - """Submit an extrinsic to manually claim accumulated root dividends from one or more subnets. - - Parameters: - wallet: Bittensor `Wallet` instance. - netuids: Iterable of subnet IDs to claim from in this call (the chain enforces a maximum number per - transaction). - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: Number of blocks during which the transaction remains valid after submission. If the extrinsic is - not included in a block within this window, it will expire and be rejected. - raise_error: Whether to raise a Python exception instead of returning a failed `ExtrinsicResponse`. - wait_for_inclusion: Whether to wait until the extrinsic is included in a block before returning. - wait_for_finalization: Whether to wait for finalization of the extrinsic in a block before returning. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` describing the result of the extrinsic execution. - - Notes: - - Only Alpha dividends are claimed; the underlying TAO stake on the Root Subnet remains unchanged. - - The current root claim type (`Swap` or `Keep`) determines whether claimed Alpha is converted to - TAO and restaked on root or left as Alpha on the originating subnets. - - See: - - See also: - - Transaction fees: - """ - return claim_root_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def commit_weights( - self, - wallet: "Wallet", - netuid: int, - salt: Salt, - uids: UIDs, - weights: Weights, - mechid: int = 0, - version_key: int = version_as_int, - max_attempts: int = 5, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = 16, - raise_error: bool = True, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This action serves as a commitment or snapshot of the neuron's current weight distribution. - - Parameters: - wallet: The wallet associated with the neuron committing the weights. - netuid: The unique identifier of the subnet. - salt: list of randomly generated integers as salt to generated weighted hash. - uids: NumPy array of neuron UIDs for which weights are being committed. - weights: NumPy array of weight values corresponding to each UID. - mechid: Subnet mechanism unique identifier. - version_key: Version key for compatibility with the network. - max_attempts: The number of maximum attempts to commit weights. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in - time, enhancing transparency and accountability within the Bittensor network. - - Notes: - - Rate Limits: - """ - attempt = 0 - response = ExtrinsicResponse(False) - - if attempt_check := validate_max_attempts(max_attempts, response): - return attempt_check - - logging.debug( - f"Committing weights with params: " - f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " - f"version_key=[blue]{version_key}[/blue]" - ) - - while attempt < max_attempts and response.success is False: - try: - response = commit_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mechid=mechid, - uids=uids, - weights=weights, - salt=salt, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - except Exception as error: - return ExtrinsicResponse.from_exception( - raise_error=raise_error, error=error - ) - attempt += 1 - - if not response.success: - logging.debug( - "No one successful attempt made. " - "Perhaps it is too soon to commit weights!" - ) - return response - - def contribute_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - amount: "Balance", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Contributes TAO to an active crowdloan campaign. - - Contributions must occur before the crowdloan's end block and are subject to minimum contribution - requirements. If a contribution would push the total raised above the cap, it is automatically clipped - to fit the remaining amount. Once the cap is reached, further contributions are rejected. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (coldkey pays, coldkey receives emissions). - crowdloan_id: The unique identifier of the crowdloan to contribute to. - amount: Amount to contribute (TAO). Must meet or exceed the campaign's `min_contribution`. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Contributions can be withdrawn before finalization via `withdraw_crowdloan`. - - If the campaign does not reach its cap by the end block, contributors can be refunded via `refund_crowdloan`. - - Contributions are counted toward `MaxContributors` limit per crowdloan. - - - Crowdloans Overview: - - Crowdloan Tutorial: - """ - return contribute_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - amount=amount, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def create_crowdloan( - self, - wallet: "Wallet", - deposit: "Balance", - min_contribution: "Balance", - cap: "Balance", - end: int, - call: Optional["GenericCall"] = None, - target_address: Optional[str] = None, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Creates a new crowdloan campaign on-chain. - - Parameters: - wallet: Bittensor Wallet instance used to sign the transaction. - deposit: Initial deposit in RAO from the creator. - min_contribution: Minimum contribution amount. - cap: Maximum cap to be raised. - end: Block number when the campaign ends. - call: Runtime call data (e.g., subtensor::register_leased_network). - target_address: SS58 address to transfer funds to on success. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure. On success, the crowdloan ID can be extracted from the - `Crowdloan.Created` event in the response. - - Notes: - - Creator cannot update `call` or `target_address` after creation. - - Creator can update `cap`, `end`, and `min_contribution` before finalization via `update_*` methods. - - Use `get_crowdloan_next_id` to determine the ID that will be assigned to the new crowdloan. - - - Crowdloans Overview: - - Crowdloan Tutorial: - - Leasing: - """ - return create_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - deposit=deposit, - min_contribution=min_contribution, - cap=cap, - end=end, - call=call, - target_address=target_address, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def create_pure_proxy( - self, - wallet: "Wallet", - proxy_type: Union[str, "ProxyType"], - delay: int, - index: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Creates a pure proxy account. - - A pure proxy is a keyless account that can only be controlled through proxy relationships. Unlike regular - proxies, pure proxies do not have their own private keys, making them more secure for certain use cases. The - pure proxy address is deterministically generated based on the spawner account, proxy type, delay, and index. - - Parameters: - wallet: Bittensor wallet object. - proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. For - available proxy types and their permissions, see the documentation link in the Notes section below. - delay: Optionally, include a delay in blocks. The number of blocks that must elapse between announcing and - executing a proxied transaction. A delay of `0` means the pure proxy can be used immediately without any - announcement period. A non-zero delay creates a time-lock, requiring announcements before execution to give - the spawner time to review/reject. - index: A salt value (u16, range `0-65535`) used to generate unique pure proxy addresses. This should generally - be left as `0` unless you are creating batches of proxies. When creating multiple pure proxies with - identical parameters (same `proxy_type` and `delay`), different index values will produce different SS58 - addresses. This is not a sequential counter—you can use any unique values (e.g., 0, 100, 7, 42) in any - order. The index must be preserved as it's required for :meth:`kill_pure_proxy`. If creating multiple pure - proxies in a single batch transaction, each must have a unique index value. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - The pure proxy account address can be extracted from the "PureCreated" event in the response. Store the - spawner address, proxy_type, index, height, and ext_index as they are required to kill the pure proxy later - via :meth:`kill_pure_proxy`. - - Bittensor proxies: - - Polkadot proxy documentation: - """ - return create_pure_proxy_extrinsic( - subtensor=self, - wallet=wallet, - proxy_type=proxy_type, - delay=delay, - index=index, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def dissolve_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Dissolves a failed or refunded crowdloan, cleaning up storage and returning the creator's deposit. - - This permanently removes the crowdloan from on-chain storage and returns the creator's deposit. Can only - be called by the creator after all non-creator contributors have been refunded via `refund_crowdloan`. - This is the final step in the lifecycle of a failed crowdloan (one that did not reach its cap by the end - block). - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). - crowdloan_id: The unique identifier of the crowdloan to dissolve. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Only the creator can dissolve their own crowdloan. - - All non-creator contributors must be refunded first via `refund_crowdloan`. - - The creator's deposit (and any remaining contribution above deposit) is returned. - - After dissolution, the crowdloan is permanently removed from chain storage. - - - - """ - return dissolve_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def finalize_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Finalizes a successful crowdloan after the cap is fully raised and the end block has passed. - - Finalization executes the stored call (e.g., `register_leased_network`) or transfers raised funds to - the target address. For subnet lease crowdloans, this registers the subnet, creates a - `SubnetLeaseBeneficiary` proxy for the creator, and records contributor shares for pro-rata emissions - distribution. Leftover funds (after registration and proxy costs) are refunded to contributors. - - Only the creator can finalize, and finalization can only occur after both the end block is reached and - the total raised equals the cap. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). - crowdloan_id: The unique identifier of the crowdloan to finalize. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure. On success, a subnet lease is created (if applicable) - and contributor shares are recorded for emissions. - - Notes: - - Only the creator can finalize. - - Finalization requires `raised == cap` and `current_block >= end`. - - For subnet leases, emissions are swapped to TAO and distributed to contributors' coldkeys during the lease. - - Leftover cap (after subnet lock + proxy deposit) is refunded to contributors pro-rata. - - - Crowdloans Overview: - - Crowdloan Tutorial: - - Emissions Distribution: - """ - return finalize_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def kill_pure_proxy( - self, - wallet: "Wallet", - pure_proxy_ss58: str, - spawner: str, - proxy_type: Union[str, "ProxyType"], - index: int, - height: int, - ext_index: int, - force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Kills (removes) a pure proxy account. - - This method removes a pure proxy account that was previously created via :meth:`create_pure_proxy`. The `kill_pure` - call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This - method automatically handles this by executing the call via :meth:`proxy`. - - Parameters: - wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the - account that created it via :meth:`create_pure_proxy`). The spawner must have an "Any" proxy relationship - with the pure proxy. - pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was - returned in the :meth:`create_pure_proxy` response. - spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via - :meth:`create_pure_proxy`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the - proxy_type used when creating the pure proxy. - index: The salt value (u16, range `0-65535`) originally used in :meth:`create_pure_proxy` to generate this - pure proxy's address. This value, combined with `proxy_type`, `delay`, and `spawner`, uniquely - identifies the pure proxy to be killed. Must match exactly the index used during creation. - height: The block height at which the pure proxy was created. - ext_index: The extrinsic index at which the pure proxy was created. - force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. - Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the - pure proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the - spawner and the pure proxy account should be used. The spawner must have a proxy relationship of this - type (or `Any`) with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If - `None`, Substrate will automatically select an available proxy type from the spawner's proxy - relationships. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as - an `Any` proxy. This method automatically handles this by executing the call via :meth:`proxy`. The spawner - must have an `Any` proxy relationship with the pure proxy for this to work. - - See: - - Warning: - All access to this account will be lost. Any funds remaining in the pure proxy account will become - permanently inaccessible after this operation. - """ - return kill_pure_proxy_extrinsic( - subtensor=self, - wallet=wallet, - pure_proxy_ss58=pure_proxy_ss58, - spawner=spawner, - proxy_type=proxy_type, - index=index, - height=height, - ext_index=ext_index, - force_proxy_type=force_proxy_type, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def mev_submit_encrypted( - self, - wallet: "Wallet", - call: "GenericCall", - sign_with: str = "coldkey", - *, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - blocks_for_revealed_execution: int = 3, - ) -> ExtrinsicResponse: - """ - Submits an encrypted extrinsic to the MEV Shield pallet. - - This function encrypts a call using ML-KEM-768 + XChaCha20Poly1305 and submits it to the MevShield pallet. The - extrinsic remains encrypted in the transaction pool until it is included in a block and decrypted by validators. - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked, coldkey will be used for signing). - call: The GenericCall object to encrypt and submit. - sign_with: The keypair to use for signing the inner call/extrinsic. Can be either "coldkey" or "hotkey". - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You can - think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the executed event, indicating that validators - have successfully decrypted and executed the inner call. If True, the function will poll subsequent - blocks for the event matching this submission's commitment. - blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after inclusion. The - function checks blocks from start_block+1 to start_block + blocks_for_revealed_execution. Returns - immediately if the event is found before the block limit is reached. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Raises: - ValueError: If NextKey is not available in storage or encryption fails. - SubstrateRequestException: If the extrinsic fails to be submitted or included. - - Note: - The encryption uses the public key from NextKey storage, which rotates every block. The payload structure is: - payload_core = signer_bytes (32B) + nonce (u32 LE, 4B) + SCALE(call) - plaintext = payload_core + b"\\x01" + signature (64B for sr25519) - commitment = blake2_256(payload_core) - - Notes: - For detailed documentation and examples of MEV Shield protection, see: - - - For creating GenericCall objects to use with this method, see: - - """ - return submit_encrypted_extrinsic( - subtensor=self, - wallet=wallet, - call=call, - sign_with=sign_with, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - blocks_for_revealed_execution=blocks_for_revealed_execution, - ) - - def modify_liquidity( - self, - wallet: "Wallet", - netuid: int, - position_id: int, - liquidity_delta: Balance, - hotkey_ss58: Optional[str] = None, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Modifies liquidity in liquidity position by adding or removing liquidity from it. - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - position_id: The id of the position record in the pool. - liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). - hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Example: - - import bittensor as bt - - subtensor = bt.subtensor(network="local") - - my_wallet = bt.Wallet() - - # if liquidity_delta is negative - - my_liquidity_delta = Balance.from_tao(100) * -1 - - subtensor.modify_liquidity( - - wallet=my_wallet, - - netuid=123, - - position_id=2, - - liquidity_delta=my_liquidity_delta - - ) - - # if liquidity_delta is positive - - my_liquidity_delta = Balance.from_tao(120) - - subtensor.modify_liquidity( - - wallet=my_wallet, - - netuid=123, - - position_id=2, - - liquidity_delta=my_liquidity_delta - ) - - Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` - to enable/disable user liquidity. - """ - return modify_liquidity_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - position_id=position_id, - liquidity_delta=liquidity_delta, - hotkey_ss58=hotkey_ss58, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def move_stake( - self, - wallet: "Wallet", - origin_netuid: int, - origin_hotkey_ss58: str, - destination_netuid: int, - destination_hotkey_ss58: str, - amount: Optional[Balance] = None, - move_all_stake: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Moves stake to a different hotkey and/or subnet. - - Parameters: - wallet: The wallet to move stake from. - origin_netuid: The netuid of the source subnet. - origin_hotkey_ss58: The SS58 address of the source hotkey. - destination_netuid: The netuid of the destination subnet. - destination_hotkey_ss58: The SS58 address of the destination hotkey. - amount: Amount of stake to move. - move_all_stake: If `True`, moves all stake from the source hotkey to the destination hotkey. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Price Protection: - - Rate Limits: - """ - check_balance_amount(amount) - return move_stake_extrinsic( - subtensor=self, - wallet=wallet, - origin_netuid=origin_netuid, - origin_hotkey_ss58=origin_hotkey_ss58, - destination_netuid=destination_netuid, - destination_hotkey_ss58=destination_hotkey_ss58, - amount=amount, - move_all_stake=move_all_stake, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def poke_deposit( - self, - wallet: "Wallet", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Adjusts deposits made for proxies and announcements based on current values. - - This method recalculates and updates the locked deposit amounts for both proxy relationships and announcements - for the signing account. It can be used to potentially lower the locked amount if the deposit requirements have - changed (e.g., due to runtime upgrades or changes in the number of proxies/announcements). - - Parameters: - wallet: Bittensor wallet object (the account whose deposits will be adjusted). - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are - needed as it operates on the account's current state. - - When to use: - - After runtime upgrade, if deposit constants have changed. - - After removing proxies/announcements, to free up excess locked funds. - - Periodically to optimize locked deposit amounts. - """ - return poke_deposit_extrinsic( - subtensor=self, - wallet=wallet, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def proxy( - self, - wallet: "Wallet", - real_account_ss58: str, - force_proxy_type: Optional[Union[str, "ProxyType"]], - call: "GenericCall", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Executes a call on behalf of the real account through a proxy. - - This method allows a proxy account (delegate) to execute a call on behalf of the real account (delegator). The - call is subject to the permissions defined by the proxy type and must respect the delay period if one was set - when the proxy was added. - - Parameters: - wallet: Bittensor wallet object (should be the proxy account wallet). - real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, - must match one of the allowed proxy types. Can be a string or ProxyType enum value. - call: The inner call to be executed on behalf of the real account. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - The call must be permitted by the proxy type. For example, a "NonTransfer" proxy cannot execute transfer - calls. The delay period must also have passed since the proxy was added. - """ - return proxy_extrinsic( - subtensor=self, - wallet=wallet, - real_account_ss58=real_account_ss58, - force_proxy_type=force_proxy_type, - call=call, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def proxy_announced( - self, - wallet: "Wallet", - delegate_ss58: str, - real_account_ss58: str, - force_proxy_type: Optional[Union[str, "ProxyType"]], - call: "GenericCall", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Executes an announced call on behalf of the real account through a proxy. - - This method executes a call that was previously announced via :meth:`announce_proxy`. The call must match the - call_hash that was announced, and the delay period must have passed since the announcement was made. The real - account has the opportunity to review and reject the announcement before execution. - - Parameters: - wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). - delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. - real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, - must match one of the allowed proxy types. Can be a string or ProxyType enum value. - call: The inner call to be executed on behalf of the real account (must match the announced call_hash). - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - The call_hash of the provided call must match the call_hash that was announced. The announcement must not - have been rejected by the real account, and the delay period must have passed. - """ - return proxy_announced_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - real_account_ss58=real_account_ss58, - force_proxy_type=force_proxy_type, - call=call, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def refund_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Refunds contributors from a failed crowdloan campaign that did not reach its cap. - - Refunds are batched, processing up to `RefundContributorsLimit` (default 50) contributors per call. - For campaigns with more contributors, multiple calls are required. Only non-creator contributors are - refunded; the creator's deposit remains until dissolution via `dissolve_crowdloan`. - - Only the crowdloan creator can call this method for a non-finalized crowdloan. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the crowdloan creator). - crowdloan_id: The unique identifier of the crowdloan to refund. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Crowdloans Overview: - - Crowdloan Lifecycle: - - Refund and Dissolve: - """ - return refund_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def reject_proxy_announcement( - self, - wallet: "Wallet", - delegate_ss58: str, - call_hash: str, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Rejects an announcement made by a proxy delegate. - - This method allows the real account to reject an announcement made by a proxy delegate, preventing the announced - call from being executed. Once rejected, the announcement cannot be executed and the announcement deposit is - returned to the delegate. - - Parameters: - wallet: Bittensor wallet object (should be the real account wallet). - delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. - call_hash: The hash of the call that was announced and is now being rejected. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - Once rejected, the announcement cannot be executed. The delegate's announcement deposit is returned. - """ - return reject_announcement_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - call_hash=call_hash, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def register( - self, - wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. - - Parameters: - wallet: The wallet associated with the neuron to be registered. - netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. - - Notes: - - Rate Limits: - """ - return register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def register_subnet( - self, - wallet: "Wallet", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Registers a new subnetwork on the Bittensor network. - - Parameters: - wallet: The wallet to be used for subnet registration. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Rate Limits: - """ - return register_subnet_extrinsic( - subtensor=self, - wallet=wallet, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def remove_proxy_announcement( - self, - wallet: "Wallet", - real_account_ss58: str, - call_hash: str, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Removes an announcement made by a proxy account. - - This method allows the proxy account to remove its own announcement before it is executed or rejected. This - frees up the announcement deposit and prevents the call from being executed. Only the proxy account that made - the announcement can remove it. - - Parameters: - wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). - real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. - call_hash: The hash of the call that was announced and is now being removed. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - Only the proxy account that made the announcement can remove it. The real account can reject it via - :meth:`reject_proxy_announcement`, but cannot remove it directly. - """ - return remove_announcement_extrinsic( - subtensor=self, - wallet=wallet, - real_account_ss58=real_account_ss58, - call_hash=call_hash, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def remove_liquidity( - self, - wallet: "Wallet", - netuid: int, - position_id: int, - hotkey_ss58: Optional[str] = None, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Remove liquidity and credit balances back to wallet's hotkey stake. - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - position_id: The id of the position record in the pool. - hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` - extrinsic to enable/disable user liquidity. - - To get the `position_id` use `get_liquidity_list` method. - """ - return remove_liquidity_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - position_id=position_id, - hotkey_ss58=hotkey_ss58, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def remove_proxies( - self, - wallet: "Wallet", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Removes all proxy relationships for the account in a single transaction. - - This method removes all proxy relationships for the signing account in a single call, which is more efficient - than removing them one by one using :meth:`remove_proxy`. The deposit for all proxies will be returned to the - account. - - Parameters: - wallet: Bittensor wallet object. The account whose proxies will be removed (the delegator). All proxy - relationships where wallet.coldkey.ss58_address is the real account will be removed. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - This removes all proxy relationships for the account, regardless of proxy type or delegate. Use - :meth:`remove_proxy` if you need to remove specific proxy relationships selectively. - """ - return remove_proxies_extrinsic( - subtensor=self, - wallet=wallet, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def remove_proxy( - self, - wallet: "Wallet", - delegate_ss58: str, - proxy_type: Union[str, "ProxyType"], - delay: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Removes a specific proxy relationship. - - This method removes a single proxy relationship between the real account and a delegate. The parameters must - exactly match those used when the proxy was added via :meth:`add_proxy`. The deposit for this proxy will be returned - to the account. - - Parameters: - wallet: Bittensor wallet object. - delegate_ss58: The SS58 address of the delegate proxy account to remove. - proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. - delay: The number of blocks before the proxy removal takes effect. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - The delegate_ss58, proxy_type, and delay parameters must exactly match those used when the proxy was added. - Use :meth:`get_proxies_for_real_account` to retrieve the exact parameters for existing proxies. - """ - return remove_proxy_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - proxy_type=proxy_type, - delay=delay, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def reveal_weights( - self, - wallet: "Wallet", - netuid: int, - uids: UIDs, - weights: Weights, - salt: Salt, - mechid: int = 0, - max_attempts: int = 5, - version_key: int = version_as_int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = 16, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. - This action serves as a revelation of the neuron's previously committed weight distribution. - - Parameters: - wallet: Bittensor Wallet instance. - netuid: The unique identifier of the subnet. - uids: NumPy array of neuron UIDs for which weights are being revealed. - weights: NumPy array of weight values corresponding to each UID. - salt: NumPy array of salt values corresponding to the hash function. - mechid: The subnet mechanism unique identifier. - max_attempts: The number of maximum attempts to reveal weights. - version_key: Version key for compatibility with the network. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function allows neurons to reveal their previously committed weight distribution, ensuring transparency and - accountability within the Bittensor network. - - Notes: - - - - Rate Limits: - """ - attempt = 0 - response = ExtrinsicResponse(False) - - if attempt_check := validate_max_attempts(max_attempts, response): - return attempt_check - - while attempt < max_attempts and response.success is False: - try: - response = reveal_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mechid=mechid, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - except Exception as error: - return ExtrinsicResponse.from_exception( - raise_error=raise_error, error=error - ) - attempt += 1 - - if not response.success: - logging.debug("No attempt made. Perhaps it is too soon to reveal weights!") - return response - - def root_register( - self, - wallet: "Wallet", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Register neuron by recycling some TAO. - - Parameters: - wallet (bittensor_wallet.Wallet): Bittensor wallet instance. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Rate Limits: - """ - - return root_register_extrinsic( - subtensor=self, - wallet=wallet, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def root_set_pending_childkey_cooldown( - self, - wallet: "Wallet", - cooldown: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Sets the pending childkey cooldown. - - Parameters: - wallet: bittensor wallet instance. - cooldown: the number of blocks to setting pending childkey cooldown. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: This operation can only be successfully performed if your wallet has root privileges. - """ - return root_set_pending_childkey_cooldown_extrinsic( - subtensor=self, - wallet=wallet, - cooldown=cooldown, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_auto_stake( - self, - wallet: "Wallet", - netuid: int, - hotkey_ss58: str, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Sets the coldkey to automatically stake to the hotkey within specific subnet mechanism. - - Parameters: - wallet: Bittensor Wallet instance. - netuid: The subnet unique identifier. - hotkey_ss58: The SS58 address of the validator's hotkey to which the miner automatically stakes all rewards - received from the specified subnet immediately upon receipt. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - Use the `get_auto_stakes` method to get the hotkey address of the validator where auto stake is set. - """ - return set_auto_stake_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - hotkey_ss58=hotkey_ss58, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_children( - self, - wallet: "Wallet", - netuid: int, - hotkey_ss58: str, - children: list[tuple[float, str]], - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Allows a coldkey to set children-keys. - - Parameters: - wallet: bittensor wallet instance. - hotkey_ss58: The `SS58` address of the neuron's hotkey. - netuid: The netuid value. - children: A list of children with their proportions. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Rate Limits: - """ - return set_children_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - children=children, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_delegate_take( - self, - wallet: "Wallet", - hotkey_ss58: str, - take: float, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - raise_error: bool = False, - period: Optional[int] = DEFAULT_PERIOD, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Sets the delegate 'take' percentage for a neuron identified by its hotkey. - The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. - - Parameters: - wallet: bittensor wallet instance. - hotkey_ss58: The `SS58` address of the neuron's hotkey. - take: Percentage reward for the delegate. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Raises: - DelegateTakeTooHigh: Delegate take is too high. - DelegateTakeTooLow: Delegate take is too low. - DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. - HotKeyAccountNotExists: The hotkey does not exist. - NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated - with the hotkey account. - bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. - bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. - - The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of - rewards among neurons and their nominators. - - Notes: - - Rate Limits: - """ - # u16 representation of the take - take_u16 = int(take * 0xFFFF) - - current_take = self.get_delegate_take(hotkey_ss58) - current_take_u16 = int(current_take * 0xFFFF) - - if current_take_u16 == take_u16: - message = f"The take for {hotkey_ss58} is already set to {take}." - logging.debug(f"[green]{message}[/green].") - return ExtrinsicResponse(True, message) - - logging.debug(f"Updating {hotkey_ss58} take: current={current_take} new={take}") - - response = set_take_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - take=take_u16, - action="increase_take" if current_take_u16 < take_u16 else "decrease_take", - period=period, - raise_error=raise_error, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - if response.success: - return response - - logging.error(f"[red]{response.message}[/red]") - return response - - def set_root_claim_type( - self, - wallet: "Wallet", - new_root_claim_type: "Literal['Swap', 'Keep'] | RootClaimType | dict", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ): - """Submit an extrinsic to set the root claim type for the wallet's coldkey. - - The root claim type determines how future Alpha dividends from subnets are handled when they are claimed for - the wallet's coldkey: - - - `Swap`: Alpha dividends are swapped to TAO at claim time and restaked on the Root Subnet (default). - - `Keep`: Alpha dividends remain as Alpha on the originating subnets. - - Parameters: - - wallet: Bittensor `Wallet` instance. - new_root_claim_type: The new root claim type to set. Can be: - - String: "Swap" or "Keep" - - RootClaimType: RootClaimType.Swap, RootClaimType.Keep - - Dict: {"KeepSubnets": {"subnets": [1, 2, 3]}} - - Callable: RootClaimType.KeepSubnets([1, 2, 3]) - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: Number of blocks for which the transaction remains valid after submission. If the extrinsic is - not included in a block within this window, it will expire and be rejected. - raise_error: Whether to raise a Python exception instead of returning a failed `ExtrinsicResponse`. - wait_for_inclusion: Whether to wait until the extrinsic is included in a block before returning. - wait_for_finalization: Whether to wait for finalization of the extrinsic in a block before returning. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` describing the result of the extrinsic execution. - - Notes: - - This setting applies to both automatic and manual root claims going forward; it does not retroactively - change how already-claimed dividends were processed. - - Only the treatment of Alpha dividends is affected; the underlying TAO stake on the Root Subnet is - unchanged. - - See: - - See also: - """ - return set_root_claim_type_extrinsic( - subtensor=self, - wallet=wallet, - new_root_claim_type=new_root_claim_type, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_subnet_identity( - self, - wallet: "Wallet", - netuid: int, - subnet_identity: SubnetIdentity, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Sets the identity of a subnet for a specific wallet and network. - - Parameters: - wallet: The wallet instance that will authorize the transaction. - netuid: The unique ID of the network on which the operation takes place. - subnet_identity: The identity data of the subnet including attributes like name, GitHub repository, contact, - URL, discord, description, and any additional metadata. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - return set_subnet_identity_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - subnet_name=subnet_identity.subnet_name, - github_repo=subnet_identity.github_repo, - subnet_contact=subnet_identity.subnet_contact, - subnet_url=subnet_identity.subnet_url, - logo_url=subnet_identity.logo_url, - discord=subnet_identity.discord, - description=subnet_identity.description, - additional=subnet_identity.additional, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_weights( - self, - wallet: "Wallet", - netuid: int, - uids: UIDs, - weights: Weights, - mechid: int = 0, - block_time: float = 12.0, - commit_reveal_version: int = 4, - max_attempts: int = 5, - version_key: int = version_as_int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Sets the interneuronal weights for the specified neuron. This process involves specifying the influence or trust - a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized - learning architecture. - - Parameters: - wallet: The wallet associated with the subnet validator setting the weights. - netuid: The unique identifier of the subnet. - uids: The list of subnet miner neuron UIDs that the weights are being set for. - weights: The corresponding weights to be set for each UID, representing the validator's evaluation of each - miner's performance. - mechid: The subnet mechanism unique identifier. - block_time: The number of seconds for block duration. - commit_reveal_version: The version of the chain commit-reveal protocol to use. - max_attempts: The number of maximum attempts to set weights. - version_key: Version key for compatibility with the network. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function is crucial in shaping the network's collective intelligence, where each neuron's learning and - contribution are influenced by the weights it sets towards others. - - Notes: - - - - Rate Limits: - """ - attempt = 0 - response = ExtrinsicResponse(False) - if attempt_check := validate_max_attempts(max_attempts, response): - return attempt_check - - def _blocks_weight_limit() -> bool: - bslu = cast(int, self.blocks_since_last_update(netuid, cast(int, uid))) - wrl = cast(int, self.weights_rate_limit(netuid)) - return bslu > wrl - - if ( - uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) - ) is None: - return ExtrinsicResponse( - False, - f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}.", - ) - - if self.commit_reveal_enabled(netuid=netuid): - # go with `commit_reveal_weights_extrinsic` extrinsic - - while ( - attempt < max_attempts - and response.success is False - and _blocks_weight_limit() - ): - logging.debug( - f"Committing weights {weights} for subnet [blue]{netuid}[/blue]. " - f"Attempt [blue]{attempt + 1}[blue] of [green]{max_attempts}[/green]." - ) - try: - response = commit_timelocked_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mechid=mechid, - uids=uids, - weights=weights, - block_time=block_time, - commit_reveal_version=commit_reveal_version, - version_key=version_key, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - except Exception as error: - return ExtrinsicResponse.from_exception( - raise_error=raise_error, error=error - ) - attempt += 1 - else: - # go with `set_mechanism_weights_extrinsic` - - while ( - attempt < max_attempts - and response.success is False - and _blocks_weight_limit() - ): - try: - logging.debug( - f"Setting weights for subnet [blue]{netuid}[/blue]. " - f"Attempt [blue]{attempt + 1}[/blue] of [green]{max_attempts}[/green]." - ) - response = set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mechid=mechid, - uids=uids, - weights=weights, - version_key=version_key, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - except Exception as error: - return ExtrinsicResponse.from_exception( - raise_error=raise_error, error=error - ) - attempt += 1 - - if not response.success: - logging.debug( - "No one successful attempt made. Perhaps it is too soon to set weights!" - ) - return response - - def serve_axon( - self, - netuid: int, - axon: "Axon", - certificate: Optional[Certificate] = None, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Registers an `Axon` serving endpoint on the Bittensor network for a specific neuron. - - This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data - processing tasks. - - Parameters: - netuid: The unique identifier of the subnetwork. - axon: The Axon instance to be registered for serving. - certificate: Certificate to use for TLS. If `None`, no TLS will be used. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Waits for the transaction to be included in a block. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, - contributing to the collective intelligence of Bittensor. - - Notes: - - Rate Limits: - """ - return serve_axon_extrinsic( - subtensor=self, - netuid=netuid, - axon=axon, - certificate=certificate, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_commitment( - self, - wallet: "Wallet", - netuid: int, - data: str, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Commits arbitrary data to the Bittensor network by publishing metadata. - - This method allows neurons to publish arbitrary data to the blockchain, which can be used for various purposes - such as sharing model updates, configuration data, or other network-relevant information. - - - Parameters: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. - netuid (int): The unique identifier of the subnetwork. - data (str): The data to be committed to the network. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Example: - - # Commit some data to subnet 1 - - response = await subtensor.commit(wallet=my_wallet, netuid=1, data="Hello Bittensor!") - - # Commit with custom period - - response = await subtensor.commit(wallet=my_wallet, netuid=1, data="Model update v2.0", period=100) - - Note: See - """ - return publish_metadata_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - data_type=f"Raw{len(data)}", - data=data.encode(), - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def set_reveal_commitment( - self, - wallet, - netuid: int, - data: str, - blocks_until_reveal: int = 360, - block_time: Union[int, float] = 12, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Commits arbitrary data to the Bittensor network by publishing metadata. - - Parameters: - wallet: The wallet associated with the neuron committing the data. - netuid: The unique identifier of the subnetwork. - data: The data to be committed to the network. - blocks_until_reveal: The number of blocks from now after which the data will be revealed. Then number of - blocks in one epoch. - block_time: The number of seconds between each block. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: - A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. - Successful extrinsic's the "data" field contains {"encrypted": encrypted, "reveal_round": reveal_round}. - """ - - encrypted, reveal_round = get_encrypted_commitment( - data, blocks_until_reveal, block_time - ) - - data_ = {"encrypted": encrypted, "reveal_round": reveal_round} - response = publish_metadata_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - data_type="TimelockEncrypted", - data=data_, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - response.data = data_ - return response - - def start_call( - self, - wallet: "Wallet", - netuid: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start - a new subnet's emission mechanism). - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - return start_call_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def swap_stake( - self, - wallet: "Wallet", - hotkey_ss58: str, - origin_netuid: int, - destination_netuid: int, - amount: Balance, - safe_swapping: bool = False, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. - Like subnet hopping - same owner, same hotkey, just changing which subnet the stake is in. - - Parameters: - wallet: The wallet to swap stake from. - hotkey_ss58: The SS58 address of the hotkey whose stake is being swapped. - origin_netuid: The netuid from which stake is removed. - destination_netuid: The netuid to which stake is added. - amount: The amount to swap. - safe_swapping: If `True`, enables price safety checks to protect against fluctuating prices. The swap - will only execute if the price ratio between subnets doesn't exceed the rate tolerance. - allow_partial_stake: If `True` and safe_staking is enabled, allows partial stake swaps when the full amount - would exceed the price tolerance. If false, the entire swap fails if it would exceed the tolerance. - rate_tolerance: The maximum allowed increase in the price ratio between subnets - (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when - safe_staking is True. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If the - transaction is not included in a block within that number of blocks, it will expire and be rejected. You - can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price. - When `safe_swapping` is enabled, the swap will only execute if: - - With `allow_partial_stake=False`: The entire swap amount can be executed without the price ratio - increasing more than `rate_tolerance`. - - With `allow_partial_stake=True`: A partial amount will be swapped up to the point where the price ratio - would increase by `rate_tolerance`. - - Price Protection: - - Rate Limits: - """ - check_balance_amount(amount) - return swap_stake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, - amount=amount, - safe_swapping=safe_swapping, - allow_partial_stake=allow_partial_stake, - rate_tolerance=rate_tolerance, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def toggle_user_liquidity( - self, - wallet: "Wallet", - netuid: int, - enable: bool, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Allow to toggle user liquidity for specified subnet. - - Parameters: - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - enable: Boolean indicating whether to enable user liquidity. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Note: The call can be executed successfully by the subnet owner only. - """ - return toggle_user_liquidity_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - enable=enable, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def transfer( - self, - wallet: "Wallet", - destination_ss58: str, - amount: Optional[Balance], - transfer_all: bool = False, - keep_alive: bool = True, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Transfer token of amount to destination. - - Parameters: - wallet: Source wallet for the transfer. - destination_ss58: Destination address for the transfer. - amount: Number of tokens to transfer. `None` is transferring all. - transfer_all: Flag to transfer all tokens. - keep_alive: Flag to keep the connection alive. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - check_balance_amount(amount) - return transfer_extrinsic( - subtensor=self, - wallet=wallet, - destination_ss58=destination_ss58, - amount=amount, - transfer_all=transfer_all, - keep_alive=keep_alive, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def transfer_stake( - self, - wallet: "Wallet", - destination_coldkey_ss58: str, - hotkey_ss58: str, - origin_netuid: int, - destination_netuid: int, - amount: Balance, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Transfers stake from one subnet to another while changing the coldkey owner. - - Parameters: - wallet: The wallet to transfer stake from. - destination_coldkey_ss58: The destination coldkey SS58 address. - hotkey_ss58: The hotkey SS58 address associated with the stake. - origin_netuid: The source subnet UID. - destination_netuid: The destination subnet UID. - amount: Amount to transfer. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Notes: - - Price Protection: - - Rate Limits: - """ - check_balance_amount(amount) - return transfer_stake_extrinsic( - subtensor=self, - wallet=wallet, - destination_coldkey_ss58=destination_coldkey_ss58, - hotkey_ss58=hotkey_ss58, - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, - amount=amount, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def unstake( - self, - wallet: "Wallet", - netuid: int, - hotkey_ss58: str, - amount: Balance, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, - safe_unstaking: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Parameters: - wallet: The wallet associated with the neuron from which the stake is being removed. - netuid: The unique identifier of the subnet. - hotkey_ss58: The `SS58` address of the hotkey account to unstake from. - amount: The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. - allow_partial_stake: If `True` and safe_staking is enabled, allows partial unstaking when - the full amount would exceed the price tolerance. If false, the entire unstake fails if it would - exceed the tolerance. - rate_tolerance: The maximum allowed price change ratio when unstaking. For example, - 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. - safe_unstaking: If `True`, enables price safety checks to protect against fluctuating prices. The unstake - will only execute if the price change doesn't exceed the rate tolerance. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function supports flexible stake management, allowing neurons to adjust their network participation and - potential reward accruals. When safe_staking is enabled, it provides protection against price fluctuations - during the time unstake is executed and the time it is actually processed by the chain. - - Notes: - - Price Protection: - - Rate Limits: - """ - check_balance_amount(amount) - return unstake_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - hotkey_ss58=hotkey_ss58, - amount=amount, - allow_partial_stake=allow_partial_stake, - rate_tolerance=rate_tolerance, - safe_unstaking=safe_unstaking, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def unstake_all( - self, - wallet: "Wallet", - netuid: int, - hotkey_ss58: str, - rate_tolerance: Optional[float] = 0.005, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. - - Parameters: - wallet: The wallet of the stake owner. - netuid: The unique identifier of the subnet. - hotkey_ss58: The SS58 address of the hotkey to unstake from. - rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum - price decrease. If not passed (None), then unstaking goes without price limit. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - Example: - - # If you would like to unstake all stakes in all subnets safely: - - import bittensor as bt - - subtensor = bt.Subtensor() - - wallet = bt.Wallet("my_wallet") - - netuid = 14 - - hotkey = "5%SOME_HOTKEY%" - - wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) - - for stake in wallet_stakes: - - result = subtensor.unstake_all( - - wallet=wallet, - - hotkey_ss58=stake.hotkey_ss58, - - netuid=stake.netuid, - - ) - - print(result) - - # If you would like to unstake all stakes in all subnets unsafely, use rate_tolerance=None: - - import bittensor as bt - - subtensor = bt.Subtensor() - - wallet = bt.Wallet("my_wallet") - - netuid = 14 - - hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" - - wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) - - for stake in wallet_stakes: - - result = subtensor.unstake_all( - - wallet=wallet, - - hotkey_ss58=stake.hotkey_ss58, - - netuid=stake.netuid, - - rate_tolerance=None, - - ) - - print(result) - - Notes: - - Price Protection: - - Rate Limits: - """ - return unstake_all_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - hotkey_ss58=hotkey_ss58, - rate_tolerance=rate_tolerance, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def unstake_multiple( - self, - wallet: "Wallet", - netuids: UIDs, - hotkey_ss58s: list[str], - amounts: Optional[list[Balance]] = None, - unstake_all: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """ - Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts - efficiently. This function is useful for managing the distribution of stakes across multiple neurons. - - Parameters: - wallet: The wallet linked to the coldkey from which the stakes are being withdrawn. - netuids: Subnets unique IDs. - hotkey_ss58s: A list of hotkey `SS58` addresses to unstake from. - amounts: The amounts of TAO to unstake from each hotkey. If not provided, unstakes all. - unstake_all: If `True`, unstakes all tokens. Amounts are ignored. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - - This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake - management aspect of the Bittensor network. - - Notes: - - Price Protection: - - Rate Limits: - """ - return unstake_multiple_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - unstake_all=unstake_all, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def update_cap_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - new_cap: "Balance", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Updates the fundraising cap of an active (non-finalized) crowdloan. - - Allows the creator to adjust the maximum total contribution amount before finalization. The new cap - must be at least equal to the amount already raised. This is useful for adjusting campaign goals based - on contributor feedback or changing subnet costs. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). - crowdloan_id: The unique identifier of the crowdloan to update. - new_cap: The new fundraising cap (TAO). Must be `>= raised`. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Only the creator can update the cap. - - The crowdloan must not be finalized. - - The new cap must be `>=` the total funds already raised. - - - Crowdloans Overview: - - Update Parameters: - """ - return update_cap_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - new_cap=new_cap, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def update_end_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - new_end: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Updates the end block of an active (non-finalized) crowdloan. - - Allows the creator to extend (or shorten) the contribution period before finalization. The new end block - must be in the future and respect the minimum and maximum duration bounds defined in the runtime constants. - This is useful for extending campaigns that need more time to reach their cap or shortening campaigns with - sufficient contributions. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). - crowdloan_id: The unique identifier of the crowdloan to update. - new_end: The new block number at which the crowdloan will end. Must be between `MinimumBlockDuration` - (7 days = 50,400 blocks) and `MaximumBlockDuration` (60 days = 432,000 blocks) from the current block. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Only the creator can update the end block. - - The crowdloan must not be finalized. - - The new end block must respect duration bounds (`MinimumBlockDuration` to `MaximumBlockDuration`). - - - Crowdloans Overview: - - Update Parameters: - """ - return update_end_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - new_end=new_end, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def update_min_contribution_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - new_min_contribution: "Balance", - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Updates the minimum contribution amount of an active (non-finalized) crowdloan. - - Allows the creator to adjust the minimum per-contribution amount before finalization. The new value must - meet or exceed the `AbsoluteMinimumContribution` constant. This is useful for adjusting contribution - requirements based on the number of expected contributors or campaign strategy. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). - crowdloan_id: The unique identifier of the crowdloan to update. - new_min_contribution: The new minimum contribution amount (TAO). Must be `>= AbsoluteMinimumContribution`. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission. - raise_error: If `True`, raises an exception rather than returning failure in the response. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - Only the creator can update the minimum contribution. - - The crowdloan must not be finalized. - - The new minimum must be `>= AbsoluteMinimumContribution` (check via `get_crowdloan_constants`). - - - Crowdloans Overview: - - Update Parameters: - """ - return update_min_contribution_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - new_min_contribution=new_min_contribution, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - - def withdraw_crowdloan( - self, - wallet: "Wallet", - crowdloan_id: int, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = DEFAULT_PERIOD, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, - ) -> ExtrinsicResponse: - """Withdraws a contribution from an active (not yet finalized or dissolved) crowdloan. - - Contributors can withdraw their contributions at any time before finalization. For regular contributors, - the full contribution amount is returned. For the creator, only amounts exceeding the initial deposit can - be withdrawn; the deposit itself remains locked until dissolution. - - Parameters: - wallet: Bittensor wallet instance used to sign the transaction (coldkey must match a contributor). - crowdloan_id: The unique identifier of the crowdloan to withdraw from. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. - period: The number of blocks during which the transaction will remain valid after submission, after which - it will be rejected. - raise_error: If `True`, raises an exception rather than returning False in the response, in case the - transaction fails. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for finalization of the extrinsic. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - `ExtrinsicResponse` indicating success or failure, with error details if applicable. - - Notes: - - - Crowdloans Overview: - - Crowdloan Lifecycle: - - Withdraw: - """ - return withdraw_crowdloan_extrinsic( - subtensor=self, - wallet=wallet, - crowdloan_id=crowdloan_id, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) + pass From 612c12f8de31dbf72da964d3533dda38d9080e79 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Tue, 30 Dec 2025 11:10:24 -0800 Subject: [PATCH 15/40] Revert get_hyperparameter to use getattr as requested in review. --- bittensor/core/subtensor.py | 8381 ++++++++++++++++++++++++++++++++++- 1 file changed, 8291 insertions(+), 90 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 1bc8097500..8665e79327 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,123 +1,8324 @@ -""" -Implementation of the Subtensor class for interacting with the Bittensor blockchain. -""" +import copy +from datetime import datetime, timezone +from functools import lru_cache +from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Union, cast -import asyncio -import logging -from typing import Any, Optional, TYPE_CHECKING, Union +import scalecodec +from async_substrate_interface.errors import SubstrateRequestException +from async_substrate_interface.substrate_addons import RetrySyncSubstrate +from async_substrate_interface.sync_substrate import SubstrateInterface +from async_substrate_interface.types import ScaleObj +from async_substrate_interface.utils.storage import StorageKey +from bittensor_drand import get_encrypted_commitment +from bittensor_wallet.utils import SS58_FORMAT + +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + CrowdloanConstants, + CrowdloanInfo, + DelegatedInfo, + DelegateInfo, + DynamicInfo, + MetagraphInfo, + NeuronInfo, + NeuronInfoLite, + ProposalVoteData, + ProxyAnnouncementInfo, + ProxyConstants, + ProxyInfo, + ProxyType, + RootClaimType, + SelectiveMetagraphIndex, + SimSwapResult, + StakeInfo, + SubnetHyperparameters, + SubnetIdentity, + SubnetInfo, + WeightCommitInfo, + decode_account_id, +) +from bittensor.core.chain_data.chain_identity import ChainIdentity +from bittensor.core.chain_data.utils import ( + decode_block, + decode_metadata, + decode_revealed_commitment, + decode_revealed_commitment_with_hotkey, +) +from bittensor.core.config import Config +from bittensor.core.errors import ChainError +from bittensor.core.extrinsics.children import ( + root_set_pending_childkey_cooldown_extrinsic, + set_children_extrinsic, +) +from bittensor.core.extrinsics.crowdloan import ( + contribute_crowdloan_extrinsic, + create_crowdloan_extrinsic, + dissolve_crowdloan_extrinsic, + finalize_crowdloan_extrinsic, + refund_crowdloan_extrinsic, + update_cap_crowdloan_extrinsic, + update_end_crowdloan_extrinsic, + update_min_contribution_crowdloan_extrinsic, + withdraw_crowdloan_extrinsic, +) +from bittensor.core.extrinsics.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) +from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic +from bittensor.core.extrinsics.move_stake import ( + move_stake_extrinsic, + swap_stake_extrinsic, + transfer_stake_extrinsic, +) +from bittensor.core.extrinsics.proxy import ( + add_proxy_extrinsic, + announce_extrinsic, + create_pure_proxy_extrinsic, + kill_pure_proxy_extrinsic, + poke_deposit_extrinsic, + proxy_announced_extrinsic, + proxy_extrinsic, + reject_announcement_extrinsic, + remove_announcement_extrinsic, + remove_proxies_extrinsic, + remove_proxy_extrinsic, +) +from bittensor.core.extrinsics.registration import ( + burned_register_extrinsic, + register_extrinsic, + register_subnet_extrinsic, + set_subnet_identity_extrinsic, +) +from bittensor.core.extrinsics.root import ( + claim_root_extrinsic, + root_register_extrinsic, + set_root_claim_type_extrinsic, +) +from bittensor.core.extrinsics.serving import ( + publish_metadata_extrinsic, + serve_axon_extrinsic, +) +from bittensor.core.extrinsics.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, + set_auto_stake_extrinsic, +) +from bittensor.core.extrinsics.start_call import start_call_extrinsic +from bittensor.core.extrinsics.take import set_take_extrinsic +from bittensor.core.extrinsics.transfer import transfer_extrinsic +from bittensor.core.extrinsics.unstaking import ( + unstake_all_extrinsic, + unstake_extrinsic, + unstake_multiple_extrinsic, +) +from bittensor.core.extrinsics.utils import get_transfer_fn_params +from bittensor.core.extrinsics.weights import ( + commit_timelocked_weights_extrinsic, + commit_weights_extrinsic, + reveal_weights_extrinsic, + set_weights_extrinsic, +) +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import ( + DEFAULT_MEV_PROTECTION, + DEFAULT_PERIOD, + MLKEM768_PUBLIC_KEY_SIZE, + TAO_APP_BLOCK_EXPLORER, + TYPE_REGISTRY, + version_as_int, +) +from bittensor.core.types import ( + BlockInfo, + ExtrinsicResponse, + Salt, + SubtensorMixin, + UIDs, + Weights, +) +from bittensor.utils import ( + Certificate, + decode_hex_identity_dict, + format_error_message, + get_caller_name, + get_mechid_storage_index, + is_valid_ss58_address, + u16_normalized_float, + u64_normalized_float, + validate_max_attempts, +) +from bittensor.utils.balance import ( + Balance, + FixedPoint, + check_balance_amount, + fixed_to_float, +) +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import ( + LiquidityPosition, + calculate_fees, + get_fees, + price_to_tick, + tick_to_price, +) -from bittensor.core.errors import NetworkQueryError -from bittensor.core.settings import version_as_int -from bittensor.utils.btlogging import logging as logger -from bittensor.utils.registration import POWSolution if TYPE_CHECKING: - from bittensor.core.chain_data import ( - NeuronInfo, - NeuronInfoLite, - PrometheusInfo, - SubnetInfo, - SubnetHyperparameters, - IPInfo, - StakeInfo, - DelegateInfo, - ScheduledColdkeySwapInfo, - ) - - -class Subtensor: - """ - The Subtensor class provides an interface for interacting with the Bittensor blockchain. - - This class handles all substrate interactions and provides methods for querying - blockchain state and submitting extrinsics. + from async_substrate_interface.sync_substrate import QueryMapResult + from bittensor_wallet import Keypair, Wallet + from scalecodec.types import GenericCall + + +class Subtensor(SubtensorMixin): + """Synchronous interface for interacting with the Bittensor blockchain. + + This class provides a thin layer over the Substrate Interface offering synchronous functionality for Bittensor. This + includes frequently-used calls for querying blockchain data, managing stakes and liquidity positions, registering + neurons, submitting weights, and many other functions for participating in Bittensor. + + Notes: + Key Bittensor concepts used throughout this class: + + - **Coldkey**: The key pair corresponding to a user's overall wallet. Used to transfer, stake, manage subnets. + - **Hotkey**: A key pair (each wallet may have zero, one, or more) used for neuron operations (mining and + validation). + - **Netuid**: Unique identifier for a subnet (0 is the Root Subnet) + - **UID**: Unique identifier for a neuron registered to a hotkey on a specific subnet. + - **Metagraph**: Data structure containing the complete state of a subnet at a block. + - **TAO**: The base network token; subnet 0 stake is in TAO + - **Alpha**: Subnet-specific token representing some quantity of TAO staked into a subnet. + - **Rao**: Smallest unit of TAO (1 TAO = 1e9 Rao) + - Bittensor Glossary + - Wallets, Coldkeys and Hotkeys in Bittensor + """ def __init__( self, network: Optional[str] = None, - config: Optional[dict] = None, + config: Optional[Config] = None, + log_verbose: bool = False, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + archive_endpoints: Optional[list[str]] = None, + mock: bool = False, + ): + """Initializes a Subtensor instance for blockchain interaction. + + Parameters: + network: The network name to connect to (e.g., `finney` for Bittensor mainnet, `test`, for + Bittensor test network, `local` for a locally deployed blockchain). If `None`, uses the + default network from config. + config: Configuration object for the Subtensor instance. If `None`, uses the default configuration. + log_verbose: Enables or disables verbose logging. + fallback_endpoints: List of fallback WebSocket endpoints to use if the primary network endpoint is + unavailable. These are tried in order when the default endpoint fails. + retry_forever: Whether to retry connection attempts indefinitely on connection errors. + mock: Whether this is a mock instance. FOR TESTING ONLY. + archive_endpoints: List of archive node endpoints for queries requiring historical block data beyond the + retention period of lite nodes. These are only used when requesting blocks that the current node is + unable to serve. + + Returns: + None + """ + if config is None: + config = self.config() + self._config = copy.deepcopy(config) + self.chain_endpoint, self.network = self.setup_config(network, self._config) + + self.log_verbose = log_verbose + self._check_and_log_network_settings() + + logging.debug( + f"Connecting to network: [blue]{self.network}[/blue], " + f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..." + ) + self.substrate = self._get_substrate( + fallback_endpoints=fallback_endpoints, + retry_forever=retry_forever, + _mock=mock, + archive_endpoints=archive_endpoints, + ) + if self.log_verbose: + logging.set_trace() + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + + def close(self): + """Closes the connection to the blockchain. + + Use this to explicitly clean up resources and close the network connection instead of waiting for garbage + collection. + + Returns: + None + + Example: + + sub = bt.Subtensor(network="finney") + + # calls to subtensor + + sub.close() + + """ + self.substrate.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # Helpers ========================================================================================================== + + def _decode_crowdloan_entry( + self, + crowdloan_id: int, + data: dict, + block_hash: Optional[str] = None, + ) -> "CrowdloanInfo": + """ + Internal helper to parse and decode a single Crowdloan record. + + Automatically decodes the embedded `call` field if present (Inline SCALE format). + """ + call_data = data.get("call") + if call_data and "Inline" in call_data: + try: + inline_bytes = bytes(call_data["Inline"][0][0]) + decoded_call = self.substrate.create_scale_object( + type_string="Call", + data=scalecodec.ScaleBytes(inline_bytes), + block_hash=block_hash, + ).decode() + data["call"] = decoded_call + except Exception as e: + data["call"] = {"decode_error": str(e), "raw": call_data} + + return CrowdloanInfo.from_dict(crowdloan_id, data) + + @lru_cache(maxsize=128) + def _get_block_hash(self, block_id: int): + return self.substrate.get_block_hash(block_id) + + def _get_substrate( + self, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + ) -> Union[SubstrateInterface, RetrySyncSubstrate]: + """Creates the Substrate instance based on provided arguments. + + This internal method creates either a standard SubstrateInterface or a RetrySyncSubstrate depending on + whether fallback/archive endpoints or infinite retry is requested. + + When `fallback_endpoints`, `archive_endpoints`, or `retry_forever` are provided, a RetrySyncSubstrate + is created with automatic failover and exponential backoff retry logic. Otherwise, a standard + SubstrateInterface is used. + + Parameters: + fallback_endpoints: List of fallback WebSocket endpoints to use if the primary endpoint is unavailable. + retry_forever: Whether to retry connection attempts indefinitely on connection errors. + _mock: Whether this is a mock instance. Used primarily for testing purposes. + archive_endpoints: List of archive node endpoints for historical block queries. Archive nodes maintain full + block history, while lite nodes only keep recent blocks. Use these when querying blocks older than the + lite node's retention period (typically a few thousand blocks). + + Returns: + Either SubstrateInterface (simple connection) or RetrySyncSubstrate (with failover and retry logic). + """ + if fallback_endpoints or retry_forever or archive_endpoints: + return RetrySyncSubstrate( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + fallback_chains=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, + archive_nodes=archive_endpoints, + ) + return SubstrateInterface( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ) + + def determine_block_hash(self, block: Optional[int]) -> Optional[str]: + """Determine the block hash for the block specified with the provided parameters. + + Ensures that only one of the block specification parameters is used and returns the appropriate block hash + for blockchain queries. + + Parameters: + block: The block number to get the hash for. If `None`, returns `None`. + + Returns: + The block hash (hex string with `0x` prefix) if one can be determined, `None` otherwise. + + Notes: + - + """ + if block is None: + return None + else: + return self.get_block_hash(block=block) + + def _runtime_method_exists(self, api: str, method: str, block_hash: str) -> bool: + """ + Check if a runtime call method exists at the given block. + + The complicated logic here comes from the fact that there are two ways in which runtime calls + are stored: the new and primary method is through the Metadata V15, but the V14 is a good backup (implemented + around mid 2024) + + Returns: + True if the runtime call method exists, False otherwise. + """ + runtime = self.substrate.init_runtime(block_hash=block_hash) + if runtime.metadata_v15 is not None: + metadata_v15_value = runtime.metadata_v15.value() + apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]} + try: + api_entry = apis[api] + methods = {entry["name"]: entry for entry in api_entry["methods"]} + _ = methods[method] + return True + except KeyError: + return False + else: + try: + self.substrate.get_metadata_runtime_call_function( + api=api, + method=method, + block_hash=block_hash, + ) + return True + except ValueError: + return False + + def _query_with_fallback( + self, + *args: tuple[str, str, Optional[list[Any]]], + block_hash: Optional[str] = None, + default_value: Any = ValueError, ): """ - Initialize the Subtensor instance. - - Args: - network: Network name to connect to - config: Configuration dictionary - _mock: Whether to use mock substrate interface + Queries the subtensor node with a given set of args, falling back to the next group if the method + does not exist at the given block. This method exists to support backwards compatibility for blocks. + + Parameters: + *args: Tuples containing (module, storage_function, params) in the order they should be attempted. + block_hash: The hash of the block being queried. If not provided, the chain tip will be used. + default_value: The default value to return if none of the methods exist at the given block. + + Returns: + The value returned by the subtensor node, or the default value if none of the methods exist at the given + block. + + Raises: + ValueError: If no default value is provided, and none of the methods exist at the given block, a + ValueError will be raised. + + Example: + + value = self._query_with_fallback( + + # the first attempt will be made to SubtensorModule.MechanismEmissionSplit with params [1] + + ("SubtensorModule", "MechanismEmissionSplit", [1]), + + # if it does not exist at the given block, the next attempt will be made to + + # SubtensorModule.MechanismEmission with params None + + ("SubtensorModule", "MechanismEmission", None), + + block_hash="0x1234", + + # if none of the methods exist at the given block, the default value of None will be returned + + default_value=None, + + ) + """ - self.network = network - self.config = config or {} - self._mock = _mock - self.substrate = None - - def _get_hyperparameter( + if block_hash is None: + block_hash = self.substrate.get_chain_head() + for module, storage_function, params in args: + if self.substrate.get_metadata_storage_function( + module_name=module, + storage_name=storage_function, + block_hash=block_hash, + ): + return self.substrate.query( + module=module, + storage_function=storage_function, + block_hash=block_hash, + params=params, + ) + if not isinstance(default_value, ValueError): + return default_value + else: + raise default_value + + def _runtime_call_with_fallback( self, - param_name: str, - netuid: int, - block: Optional[int] = None, - reuse_block: bool = False, - ) -> Optional[Any]: + *args: tuple[str, str, Optional[list[Any]] | dict[str, Any]], + block_hash: Optional[str] = None, + default_value: Any = ValueError, + ): + """ + Makes a runtime call to the subtensor node with a given set of args, falling back to the next group if the + api.method does not exist at the given block. This method exists to support backwards compatibility for blocks. + + Parameters: + *args: Tuples containing (api, method, params) in the order they should be attempted. + block_hash: The hash of the block being queried. If not provided, the chain tip will be used. + default_value: The default value to return if none of the methods exist at the given block. + + Raises: + ValueError: If no default value is provided, and none of the methods exist at the given block, a + ValueError will be raised. + + Example: + + query = self._runtime_call_with_fallback( + + # the first attempt will be made to SubnetInfoRuntimeApi.get_selective_mechagraph with the + + # given params + + ( + + "SubnetInfoRuntimeApi", + + "get_selective_mechagraph", + + [netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]], + + ), + + # if it does not exist at the given block, the next attempt will be made as such: + + ("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]), + + block_hash=block_hash, + + # if none of the methods exist at the given block, the default value will be returned + + default_value=None, + + ) + """ - Internal method to retrieve a hyperparameter for a specific subnet. - - Args: - param_name: Name of the hyperparameter to retrieve - netuid: Unique identifier for the subnet - block: Block number to query at (None for latest) - reuse_block: Whether to reuse cached block - + if block_hash is None: + block_hash = self.substrate.get_chain_head() + for api, method, params in args: + if self._runtime_method_exists( + api=api, method=method, block_hash=block_hash + ): + return self.substrate.runtime_call( + api=api, + method=method, + block_hash=block_hash, + params=params, + ) + if not isinstance(default_value, ValueError): + return default_value + else: + raise default_value + + def get_hyperparameter( + self, param_name: str, netuid: int, block: Optional[int] = None + ) -> Optional[Any]: + """Retrieves a specified hyperparameter for a specific subnet. + + This method queries the blockchain for subnet-specific hyperparameters such as difficulty, tempo, immunity + period, and other network configuration values. Return types and units vary by parameter. + + Parameters: + param_name: The name of the hyperparameter storage function to retrieve. + netuid: The unique identifier of the subnet. + block: The block number to query. If `None`, queries the current chain head. + Returns: - The hyperparameter value, or None if not found + The value of the specified hyperparameter if the subnet exists, `None` otherwise. Return type varies + by parameter (int, float, bool, or Balance). + + Notes: + - """ - if self.substrate is None: - raise RuntimeError("Substrate connection not initialized") - + block_hash = self.determine_block_hash(block) + if not self.subnet_exists(netuid, block=block): + logging.error(f"subnet {netuid} does not exist") + return None + result = self.substrate.query( module="SubtensorModule", storage_function=param_name, params=[netuid], - block_hash=None if block is None else self.substrate.get_block_hash(block), - reuse_block_hash=reuse_block, + block_hash=block_hash, ) - - return getattr(result, "value", result) - - def get_subnet_hyperparameters( + + if result is None: + return None + + if hasattr(result, "value"): + return result.value + + return result + + @property + def block(self) -> int: + """Provides an asynchronous getter to retrieve the current block number. + + Returns: + The current blockchain block number. + """ + return self.get_current_block() + + def sim_swap( self, - netuid: int, + origin_netuid: int, + destination_netuid: int, + amount: "Balance", + block: Optional[int] = None, + ) -> SimSwapResult: + """Simulates a swap/stake operation and calculates the fees and resulting amounts. + + This method queries the SimSwap Runtime API to calculate the swap fees (in Alpha or TAO) and the quantities + of Alpha or TAO tokens expected as output from the transaction. This simulation does NOT include the + blockchain extrinsic transaction fee (the fee to submit the transaction itself). + + When moving stake between subnets, the operation may involve swapping Alpha (subnet-specific stake token) to + TAO (the base network token), then TAO to Alpha on the destination subnet. For subnet 0 (root network), all + stake is in TAO. + + Parameters: + origin_netuid: Netuid of the source subnet (0 if adding stake). + destination_netuid: Netuid of the destination subnet. + amount: Amount to swap/stake as a Balance object. Use `Balance.from_tao(...)` or + `Balance.from_rao(...)` to create the amount. + block: The block number to query. If `None`, uses the current chain head. + + Returns: + SimSwapResult: Object containing `alpha_fee`, `tao_fee`, `alpha_amount`, and `tao_amount` fields + representing the swap fees and output amounts. + + Example: + + # Simulate staking 100 TAO stake to subnet 1 + + result = subtensor.sim_swap( + + origin_netuid=0, + + destination_netuid=1, + + amount=Balance.from_tao(100) + + ) + + print(f"Fee: {result.tao_fee.tao} TAO, Output: {result.alpha_amount} Alpha") + + Notes: + - **Alpha**: Subnet-specific stake token (dynamic TAO) + - **TAO**: Base network token; subnet 0 uses TAO directly + - The returned fees do NOT include the extrinsic transaction fee + + - Transaction Fees: + - Glossary: + """ + check_balance_amount(amount) + if origin_netuid > 0 and destination_netuid > 0: + # for cross-subnet moves where neither origin nor destination is root + intermediate_result_ = self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block=block, + ) + sn_price = self.get_subnet_price(origin_netuid, block=block) + intermediate_result = SimSwapResult.from_dict( + intermediate_result_, origin_netuid + ) + result = SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={ + "netuid": destination_netuid, + "tao": intermediate_result.tao_amount.rao, + }, + block=block, + ), + origin_netuid, + ) + secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) + result.alpha_fee = result.alpha_fee + secondary_fee + return result + elif origin_netuid > 0: + # dynamic to tao + return SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block=block, + ), + origin_netuid, + ) + else: + # tao to dynamic or unstaked to staked tao (SN0) + return SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={"netuid": destination_netuid, "tao": amount.rao}, + block=block, + ), + destination_netuid, + ) + + # Subtensor queries =========================================================================================== + + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional["ScaleObj"]: + """Retrieves a constant from the specified module on the Bittensor blockchain. + + Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot + be accessed through other, standard getter methods. + + Parameters: + module_name: The name of the module containing the constant (e.g., `Balances`, `SubtensorModule`). + constant_name: The name of the constant to retrieve (e.g., `ExistentialDeposit`). + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A SCALE-decoded object if found, `None` otherwise. Access the actual value using `.value` attribute. + Common types include int (for counts/blocks), Balance objects (for amounts in Rao), and booleans. + + """ + return self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=self.determine_block_hash(block), + ) + + def query_map( + self, + module: str, + name: str, + params: Optional[list] = None, + block: Optional[int] = None, + ) -> "QueryMapResult": + """Queries map storage from any module on the Bittensor blockchain. + + Use this function for nonstandard queries to map storage defined within the Bittensor blockchain, if these cannot + be accessed through other, standard getter methods. + + Parameters: + module: The name of the module from which to query the map storage (e.g., "SubtensorModule", "System"). + name: The specific storage function within the module to query (e.g., "Bonds", "Weights"). + params: Parameters to be passed to the query. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + QueryMapResult: A data structure representing the map storage if found, None otherwise. + """ + result = self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block=block), + ) + return result + + def query_map_subtensor( + self, + name: str, + params: Optional[list] = None, + block: Optional[int] = None, + ) -> "QueryMapResult": + """Queries map storage from the Subtensor module on the Bittensor blockchain. + + Use this function for nonstandard queries to map storage defined within the Bittensor blockchain, if these cannot + be accessed through other, standard getter methods. + + Parameters: + name: The name of the map storage function to query. + params: A list of parameters to pass to the query function. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + An object containing the map-like data structure, or `None` if not found. + """ + return self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def query_module( + self, + module: str, + name: str, + params: Optional[list] = None, + block: Optional[int] = None, + ) -> Optional[Union["ScaleObj", Any, FixedPoint]]: + """Queries any module storage on the Bittensor blockchain with the specified parameters and block number. + This function is a generic query interface that allows for flexible and diverse data retrieval from various + blockchain modules. Use this function for nonstandard queries to storage defined within the Bittensor + blockchain, if these cannot be accessed through other, standard getter methods. + + Parameters: + module: The name of the module from which to query data. + name: The name of the storage function within the module. + params: A list of parameters to pass to the query function. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + An object containing the requested data if found, `None` otherwise. + + """ + return self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def query_runtime_api( + self, + runtime_api: str, + method: str, + params: Optional[Union[list[Any], dict[str, Any]]] = None, + block: Optional[int] = None, + ) -> Any: + """Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime + and retrieve data encoded in Scale Bytes format. Use this function for nonstandard queries to the runtime + environment, if these cannot be accessed through other, standard getter methods. + + Parameters: + runtime_api: The name of the runtime API to query. + method: The specific method within the runtime API to call. + params: The parameters to pass to the method call. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The decoded result from the runtime API call, or `None` if the call fails. + + """ + block_hash = self.determine_block_hash(block) + + return self.substrate.runtime_call( + api=runtime_api, + method=method, + params=params, + block_hash=block_hash, + ).value + + def query_subtensor( + self, + name: str, + params: Optional[list] = None, block: Optional[int] = None, - reuse_block: bool = False, - ) -> Optional["SubnetHyperparameters"]: + ) -> Optional[Union["ScaleObj", Any]]: + """Queries named storage from the Subtensor module on the Bittensor blockchain. + + Use this function for nonstandard queries to constants defined within the Bittensor blockchain, if these cannot + be accessed through other, standard getter methods. + + Parameters: + name: The name of the storage function to query. + params: A list of parameters to pass to the query function. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + query_response: An object containing the requested data. + """ + return self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def state_call( + self, method: str, data: str, block: Optional[int] = None + ) -> dict[Any, Any]: + """Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. + This function is typically used for advanced, nonstandard queries not provided by other getter methods. + + Use this method when you need to query runtime APIs or storage functions that don't have dedicated + wrapper methods in the SDK. For standard queries, prefer the specific getter methods (e.g., `get_balance`, + `get_stake`) which provide better type safety and error handling. + + Parameters: + method: The runtime API method name (e.g., "SubnetInfoRuntimeApi", "get_metagraph"). + data: Hex-encoded string of the SCALE-encoded parameters to pass to the method. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The result of the rpc call. + + """ + block_hash = self.determine_block_hash(block) + return self.substrate.rpc_request( + method="state_call", params=[method, data], block_hash=block_hash + ) + + # Common subtensor calls =========================================================================================== + + def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo"]]: + """Queries the blockchain for comprehensive information about all subnets, including their dynamic parameters + and operational status. + + Parameters: + block: The block number to query. If `None`, queries the current chain head. + + Returns: + Optional[list[DynamicInfo]]: A list of `DynamicInfo` objects, each containing detailed information about + a subnet, or None if the query fails. + """ + block_hash = self.determine_block_hash(block=block) + query = self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=block_hash, + ) + decoded = query.decode() + try: + subnet_prices = self.get_subnet_prices(block=block) + for sn in decoded: + sn.update( + {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} + ) + except (SubstrateRequestException, ValueError) as e: + logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") + + return DynamicInfo.list_from_dicts(decoded) + + def blocks_since_last_step( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Queries the blockchain to determine how many blocks have passed since the last epoch step for a specific + subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The number of blocks since the last step in the subnet, or None if the query fails. + + Notes: + - + """ + query = self.query_subtensor( + name="BlocksSinceLastStep", block=block, params=[netuid] + ) + return query.value if query is not None and hasattr(query, "value") else query + + def blocks_since_last_update( + self, netuid: int, uid: int, block: Optional[int] = None + ) -> Optional[int]: + """Returns the number of blocks since the last update, or `None` if the subnetwork or UID does not exist. + + Parameters: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + block: The block number for this query. If `None`, queries the current chain head. + + Returns: + The number of blocks since the last update, or None if the subnetwork or UID does not exist. """ - Retrieve hyperparameters for a specific subnet. - - Args: - netuid: Unique identifier for the subnet - block: Block number to query at (None for latest) - reuse_block: Whether to reuse cached block - + block = block or self.get_current_block() + call = self.get_hyperparameter( + param_name="LastUpdate", netuid=netuid, block=block + ) + return None if not call else (block - int(call[uid])) + + def blocks_until_next_epoch( + self, netuid: int, tempo: Optional[int] = None, block: Optional[int] = None + ) -> Optional[int]: + """Returns the number of blocks until the next epoch of subnet with provided netuid. + + Parameters: + netuid: The unique identifier of the subnetwork. + tempo: The tempo of the subnet. + block: the block number for this query. + Returns: - SubnetHyperparameters object or None if subnet doesn't exist + The number of blocks until the next epoch of the subnet with provided netuid. """ - pass - - def get_commitment( + block = block or self.block + + tempo = tempo or self.tempo(netuid=netuid) + if not tempo: + return None + + # the logic is the same as in SubtensorModule:blocks_until_next_epoch + netuid_plus_one = int(netuid) + 1 + tempo_plus_one = tempo + 1 + adjusted_block = (block + netuid_plus_one) % (2**64) + remainder = adjusted_block % tempo_plus_one + return tempo - remainder + + def bonds( self, netuid: int, - uid: int, + mechid: int = 0, block: Optional[int] = None, - ) -> Optional[str]: + ) -> list[tuple[int, list[tuple[int, int]]]]: + """Retrieves the bond distribution set by subnet validators within a specific subnet. + + Bonds represent a validator's accumulated assessment of each miner's performance over time, which serves as the + starting point of Yuma Consensus. + + Parameters: + netuid: Subnet identifier. + mechid: Subnet mechanism identifier (default 0 for primary mechanism). + block: The block number for this query. If `None`, queries the current chain head. + + Returns: + List of tuples, where each tuple contains: + - validator_uid: The UID of the validator + - bonds: List of (miner_uid, bond_value) pairs + + Bond values are u16-normalized (0-65535, where 65535 = 1.0 or 100%). + + Example: + + # Get bonds for subnet 1 + + bonds = subtensor.bonds(netuid=1) + + print(bonds[0]) + + # example output: (5, [(0, 32767), (1, 16383), (3, 8191)]) + + # This means validator UID 5 has bonds: 50% to miner 0, 25% to miner 1, 12.5% to miner 3 + + Notes: + - See: + - See: + """ + storage_index = get_mechid_storage_index(netuid, mechid) + b_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Bonds", + params=[storage_index], + block_hash=self.determine_block_hash(block), + ) + b_map = [] + for uid, b in b_map_encoded: + if b.value is not None: + b_map.append((uid, b.value)) + + return b_map + + def commit_reveal_enabled(self, netuid: int, block: Optional[int] = None) -> bool: + """Check if commit-reveal mechanism is enabled for a given subnet at a specific block. + + Parameters: + netuid: The unique identifier of the subnet for which to check the commit-reveal mechanism. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + True if commit-reveal mechanism is enabled, False otherwise. + + Notes: + - + - """ - Retrieve the commitment for a specific neuron. - - Args: - netuid: Unique identifier for the subnet - uid: Unique identifier for the neuron - block: Block number to query at (None for latest) - + call = self.get_hyperparameter( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + ) + return True if call is True else False + + def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. + + This parameter determines the computational challenge required for neurons to participate in consensus and + validation processes, using proof of work (POW) registration. + + Parameters: + netuid: The unique identifier of the subnet. + block: The block number to query. If `None`, queries the current chain head. + Returns: - The commitment string, or None if not found + The value of the 'Difficulty' hyperparameter if the subnet exists, `None` otherwise. + + Notes: + Burn registration is much more common on Bittensor subnets currently, compared to POW registration. + + - + - + - """ - pass + call = self.get_hyperparameter( + param_name="Difficulty", netuid=netuid, block=block + ) + if call is None: + return None + return int(call) + + def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + """Returns true if the hotkey has been associated with a coldkey through account creation. + + This method queries the Subtensor's Owner storage map to check if the hotkey has been paired with a + coldkey, as it must be before it (the hotkey) can be used for neuron registration. + + The Owner storage map defaults to the zero address (`5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM`) + for unused hotkeys. This method returns `True` if the Owner value is anything other than this default. + + Parameters: + hotkey_ss58: The SS58 address of the hotkey. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + True if the hotkey has been associated with a coldkey, False otherwise. + + Notes: + - + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return_val = ( + False + if result is None + # not the default key (0x0) + else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) + return return_val + + def get_admin_freeze_window(self, block: Optional[int] = None) -> int: + """Returns the duration, in blocks, of the administrative freeze window at the end of each epoch. + + The admin freeze window is a period at the end of each epoch during which subnet owner + operations are prohibited. This prevents subnet owners from modifying hyperparameters or performing certain + administrative actions right before validators submit weights at the epoch boundary. + + Parameters: + block: The block number to query. + + Returns: + The number of blocks in the administrative freeze window (default: 10 blocks, ~2 minutes). + + Notes: + - + """ + + return self.substrate.query( + module="SubtensorModule", + storage_function="AdminFreezeWindow", + block_hash=self.determine_block_hash(block), + ).value + + def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: + """Retrieves detailed information about all subnets within the Bittensor network. + + Parameters: + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A list of SubnetInfo objects, each containing detailed information about a subnet. + + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + ) + if not result: + return [] + try: + subnets_prices = self.get_subnet_prices(block=block) + + for subnet in result: + subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)}) + except (SubstrateRequestException, ValueError) as e: + logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") + + return SubnetInfo.list_from_dicts(result) + + def get_all_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, str]: + """Retrieves raw commitment metadata from a given subnet. + + This method retrieves all commitment data for all neurons in a specific subnet. This is useful for analyzing the + commit-reveal patterns across an entire subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A mapping of the ss58:commitment with the commitment as a string. + + Example: + + # TODO add example of how to handle realistic commitment data + """ + query = self.query_map( + module="Commitments", + name="CommitmentOf", + params=[netuid], + block=block, + ) + result = {} + for id_, value in query: + try: + result[decode_account_id(id_[0])] = decode_metadata(value) + except Exception as error: + logging.error( + f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" + ) + return result + + def get_all_ema_tao_inflow( + self, + block: Optional[int] = None, + ) -> dict[int, tuple[int, Balance]]: + """Retrieves the EMA (exponential moving average) of net TAO flows for all subnets. + + The EMA tracks net TAO flows (staking minus unstaking) with a 30-day half-life (~86.8 day window), smoothing + out short-term fluctuations while capturing sustained staking trends. This metric determines the subnet's share + of TAO emissions under the current, flow-based model. Positive values indicate net inflow (more staking than unstaking), + negative values indicate net outflow. Subnets with negative EMA flows receive zero emissions. + + Parameters: + block: The block number to retrieve the commitment from. + + Returns: + Dict mapping netuid to (last_updated_block, ema_flow). The Balance represents the EMA of net TAO flow in + TAO units. Positive values indicate sustained net inflow, negative values indicate sustained net outflow. + + The EMA uses a smoothing factor α ≈ 0.000003209, creating a 30-day half-life and ~86.8 day window. Only + direct stake/unstake operations count toward flows; neuron registrations and root claims are excluded. + Subnet 0 (root network) does not have an EMA TAO flow value. + + Notes: + - Flow-based emissions: + - EMA smoothing: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query_map( + module="SubtensorModule", + storage_function="SubnetEmaTaoFlow", + block_hash=block_hash, + ) + tao_inflow_ema = {} + for netuid, (block_updated, tao_bits) in query: + ema_value = int(fixed_to_float(tao_bits)) + tao_inflow_ema[netuid] = (block_updated, Balance.from_rao(ema_value)) + return tao_inflow_ema + + def get_all_metagraphs_info( + self, + all_mechanisms: bool = False, + block: Optional[int] = None, + ) -> Optional[list[MetagraphInfo]]: + """ + Retrieves a list of MetagraphInfo objects for all subnets + + Parameters: + all_mechanisms: If True then returns all mechanisms, otherwise only those with index 0 for all subnets. + block: The blockchain block number for the query. + + Returns: + List of MetagraphInfo objects for all existing subnets. + + Notes: + - + """ + block_hash = self.determine_block_hash(block) + method = "get_all_mechagraphs" if all_mechanisms else "get_all_metagraphs" + query = self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method=method, + block_hash=block_hash, + ) + if query is None or not hasattr(query, "value"): + return None + + return MetagraphInfo.list_from_dicts(query.value) + + def get_all_neuron_certificates( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, Certificate]: + """ + Retrieves the TLS certificates for neurons within a specified subnet (netuid) of the Bittensor network. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Dictionary mapping neuron hotkey SS58 addresses to their Certificate objects. Only includes neurons + that have registered certificates. + + Notes: + This method is used for certificate discovery to establish mutual TLS communication between neurons. + + - + """ + query_certificates = self.query_map( + module="SubtensorModule", + name="NeuronCertificates", + params=[netuid], + block=block, + ) + output = {} + for key, item in query_certificates: + output[decode_account_id(key)] = Certificate(item.value) + return output + + def get_all_revealed_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, tuple[tuple[int, str], ...]]: + """Retrieves all revealed commitments for a given subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A dictionary mapping hotkey addresses to tuples of (reveal_block, commitment_message) pairs. + Each validator can have multiple revealed commitments (up to 10 most recent). + + Example: + + # sample return value + + { + + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), + + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), + + } + + Notes: + - + + """ + query = self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + ) + + result = {} + for pair in query: + hotkey_ss58_address, commitment_message = ( + decode_revealed_commitment_with_hotkey(pair) + ) + result[hotkey_ss58_address] = commitment_message + return result + + def get_all_subnets_netuid(self, block: Optional[int] = None) -> UIDs: + """ + Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. + + Parameters: + block: The blockchain block number for the query. + + Returns: + A list of subnet netuids. + + This function provides a comprehensive view of the subnets within the Bittensor network, + offering insights into its diversity and scale. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=self.determine_block_hash(block), + ) + subnets = [] + if result.records: + for netuid, exists in result: + if exists: + subnets.append(netuid) + return subnets + + def get_auto_stakes( + self, + coldkey_ss58: str, + block: Optional[int] = None, + ) -> dict[int, str]: + """Fetches auto stake destinations for a given wallet across all subnets. + + Parameters: + coldkey_ss58: Coldkey ss58 address. + block: The block number for the query. If `None`, queries the current chain head. + + Returns: + Dictionary mapping netuid to hotkey, where: + + - netuid: The unique identifier of the subnet. + - hotkey: The hotkey of the wallet. + + Notes: + - + """ + block_hash = self.determine_block_hash(block=block) + query = self.substrate.query_map( + module="SubtensorModule", + storage_function="AutoStakeDestination", + params=[coldkey_ss58], + block_hash=block_hash, + ) + + pairs = {} + for netuid, destination in query: + hotkey_ss58 = decode_account_id(destination.value[0]) + if hotkey_ss58: + pairs[int(netuid)] = hotkey_ss58 + + return pairs + + def get_balance(self, address: str, block: Optional[int] = None) -> Balance: + """Retrieves the balance for given coldkey. + + This method queries the System module's Account storage to get the current balance of a coldkey address. The + balance represents the amount of TAO tokens held by the specified address. + + Parameters: + address: The coldkey address in SS58 format. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + Balance: The balance object containing the account's TAO balance. + """ + balance = self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=self.determine_block_hash(block), + ) + return Balance(balance["data"]["free"]) + + def get_balances( + self, + *addresses: str, + block: Optional[int] = None, + ) -> dict[str, Balance]: + """Retrieves the balance for given coldkey(s). + + This method efficiently queries multiple coldkey addresses in a single batch operation, returning a dictionary + mapping each address to its corresponding balance. This is more efficient than calling get_balance multiple + times. + + Parameters: + *addresses: Variable number of coldkey addresses in SS58 format. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A dictionary mapping each address to its Balance object. + """ + if not (block_hash := self.determine_block_hash(block)): + block_hash = self.substrate.get_chain_head() + calls = [ + ( + self.substrate.create_storage_key( + "System", "Account", [address], block_hash=block_hash + ) + ) + for address in addresses + ] + batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + value = item[1] or {"data": {"free": 0}} + results.update({item[0].params[0]: Balance(value["data"]["free"])}) + return results + + def get_current_block(self) -> int: + """Returns the current block number on the Bittensor blockchain. + + This function provides the latest block number, indicating the most recent state of the blockchain. + + Returns: + int: The current chain block number. + + Notes: + - + """ + return self.substrate.get_block_number(None) + + def get_block_hash(self, block: Optional[int] = None) -> str: + """Retrieves the hash of a specific block on the Bittensor blockchain. + + The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its + integrity and immutability. It is a fundamental aspect of blockchain technology, providing a secure reference + to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the + trustworthiness of the blockchain. + + Parameters: + block: The block number for which the hash is to be retrieved. If `None`, returns the latest block hash. + + Returns: + str: The cryptographic hash of the specified block. + + Notes: + - + """ + if block is not None: + return self._get_block_hash(block) + else: + return self.substrate.get_chain_head() + + def get_block_info( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + ) -> Optional[BlockInfo]: + """Retrieve complete information about a specific block from the Subtensor chain. + + This method aggregates multiple low-level RPC calls into a single structured response, returning both the raw + on-chain data and high-level decoded metadata for the given block. + + Parameters: + block: The block number for which the hash is to be retrieved. + block_hash: The hash of the block to retrieve the block from. + + Returns: + BlockInfo instance: A dataclass containing all available information about the specified block, including: + + - number: The block number. + - hash: The corresponding block hash. + - timestamp: The timestamp of the block (based on the `Timestamp.Now` extrinsic). + - header: The raw block header returned by the node RPC. + - extrinsics: The list of decoded extrinsics included in the block. + - explorer: The link to block explorer service. Always related with finney block data. + """ + block_info = self.substrate.get_block( + block_number=block, block_hash=block_hash, ignore_decoding_errors=True + ) + if isinstance(block_info, dict) and (header := block_info.get("header")): + block = block or header.get("number", None) + block_hash = block_hash or header.get("hash", None) + extrinsics = cast(list, block_info.get("extrinsics")) + timestamp = None + for ext in extrinsics: + if ext.value_serialized["call"]["call_module"] == "Timestamp": + timestamp = ext.value_serialized["call"]["call_args"][0]["value"] + break + return BlockInfo( + number=block, + hash=block_hash, + timestamp=timestamp, + header=header, + extrinsics=extrinsics, + explorer=f"{TAO_APP_BLOCK_EXPLORER}{block}", + ) + return None + + def get_children( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> tuple[bool, list[tuple[float, str]], str]: + """Retrieves the children of a given hotkey and netuid. + + This method queries the SubtensorModule's ChildKeys storage function to get the children and formats them before + returning as a tuple. It provides information about the child neurons that a validator has set for weight + distribution. + + Parameters: + hotkey_ss58: The hotkey value. + netuid: The netuid value. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A tuple containing a boolean indicating success or failure, a list of formatted children with their + proportions, and an error message (if applicable). + + Example: + + # Get children for a hotkey in subnet 1 + + success, children, error = subtensor.get_children(hotkey="5F...", netuid=1) + + if success: + + for proportion, child_hotkey in children: + + print(f"Child {child_hotkey}: {proportion}") + + Notes: + - + """ + try: + children = self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey_ss58, netuid], + block_hash=self.determine_block_hash(block), + ) + if children: + formatted_children = [] + for proportion, child in children.value: + # Convert U64 to int + formatted_child = decode_account_id(child[0]) + normalized_proportion = u64_normalized_float(proportion) + formatted_children.append((normalized_proportion, formatted_child)) + return True, formatted_children, "" + else: + return True, [], "" + except SubstrateRequestException as e: + return False, [], format_error_message(e) + + def get_children_pending( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> tuple[ + list[tuple[float, str]], + int, + ]: + """Retrieves the pending children of a given hotkey and netuid. + + This method queries the SubtensorModule's PendingChildKeys storage function to get children that are pending + approval or in a cooldown period. These are children that have been proposed but not yet finalized. + + Parameters: + hotkey_ss58: The hotkey value. + netuid: The netuid value. + block: The block number for which the children are to be retrieved. If `None`, queries the current chain head. + + Returns: + tuple: A tuple containing: + + - list[tuple[float, str]]: A list of children with their proportions. + - int: The cool-down block number. + + Notes: + - + """ + + children, cooldown = self.substrate.query( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ).value + + return ( + [ + ( + u64_normalized_float(proportion), + decode_account_id(child[0]), + ) + for proportion, child in children + ], + cooldown, + ) + + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: + """Retrieves the on-chain commitment for a specific neuron in the Bittensor network. + + This method retrieves the commitment data that a neuron has published to the blockchain. Commitments are used in + the commit-reveal mechanism for secure weight setting and other network operations. + + Parameters: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The commitment data as a string. + + + # TODO: add a real example of how to handle realistic commitment data, or chop example + + Notes: + - + """ + metagraph = self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] # type: ignore + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return "" + + metadata = cast(dict, self.get_commitment_metadata(netuid, hotkey, block)) + try: + return decode_metadata(metadata) + except Exception as error: + logging.error(error) + return "" + + def get_commitment_metadata( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ) -> Union[str, dict]: + # TODO: how to handle return data? need good example @roman + """Fetches raw commitment metadata from specific subnet for given hotkey. + + Parameters: + netuid: The unique subnet identifier. + hotkey_ss58: The hotkey ss58 address. + block: The blockchain block number for the query. + + Returns: + The raw commitment metadata. Returns a dict when commitment data exists, + or an empty string when no commitment is found for the given hotkey on the subnet. + + Notes: + - + """ + commit_data = self.substrate.query( + module="Commitments", + storage_function="CommitmentOf", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return commit_data + + def get_crowdloan_constants( + self, + constants: Optional[list[str]] = None, + block: Optional[int] = None, + ) -> "CrowdloanConstants": + """Retrieves runtime configuration constants governing crowdloan behavior and limits on the Bittensor blockchain. + + If a list of constant names is provided, only those constants will be queried. + Otherwise, all known constants defined in `CrowdloanConstants.field_names()` are fetched. + + These constants define requirements and operational limits for crowdloan campaigns: + + - `AbsoluteMinimumContribution`: Minimum amount per contribution (TAO). + - `MaxContributors`: Maximum number of unique contributors per crowdloan. + - `MaximumBlockDuration`: Maximum duration (in blocks) for a crowdloan campaign (60 days = 432,000 blocks on production). + - `MinimumDeposit`: Minimum deposit required from the creator (TAO). + - `MinimumBlockDuration`: Minimum duration (in blocks) for a crowdloan campaign (7 days = 50,400 blocks on production). + - `RefundContributorsLimit`: Maximum number of contributors refunded per `refund_crowdloan` call (typically 50). + + Parameters: + constants: Specific constant names to query. If `None`, retrieves all constants from `CrowdloanConstants`. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + A `CrowdloanConstants` data object containing the queried constants. Missing constants return `None`. + Notes: + These constants enforce contribution floors, duration bounds, and refund batching limits. + + - Crowdloans Overview: + + """ + result = {} + const_names = constants or CrowdloanConstants.constants_names() + + for const_name in const_names: + query = self.query_constant( + module_name="Crowdloan", + constant_name=const_name, + block=block, + ) + + if query is not None: + result[const_name] = query.value + + return CrowdloanConstants.from_dict(result) + + def get_crowdloan_contributions( + self, + crowdloan_id: int, + block: Optional[int] = None, + ) -> dict[str, "Balance"]: + """Retrieves all contributions made to a specific crowdloan campaign. + + Returns a mapping of contributor coldkey addresses to their contribution amounts in Rao. + + Parameters: + crowdloan_id: The unique identifier of the crowdloan. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + Dictionary mapping contributor SS58 addresses to their `Balance` contribution amounts (in Rao). + Returns empty dictionary if the crowdloan has no contributions or does not exist. + + Notes: + Contributions are clipped to the remaining cap. Once the cap is reached, no further contributions are accepted. + + - Crowdloans Overview: + - Crowdloan Tutorial: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query_map( + module="Crowdloan", + storage_function="Contributions", + params=[crowdloan_id], + block_hash=block_hash, + ) + result = {} + for record in query.records: + if record[1].value: + result[decode_account_id(record[0])] = Balance.from_rao(record[1].value) + return result + + def get_crowdloan_by_id( + self, crowdloan_id: int, block: Optional[int] = None + ) -> Optional["CrowdloanInfo"]: + """Retrieves detailed information about a specific crowdloan campaign. + + Parameters: + crowdloan_id: Unique identifier of the crowdloan (auto-incremented starting from 0). + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + `CrowdloanInfo` object containing: campaign ID, creator address, creator's deposit, + minimum contribution amount, end block, funding cap, funds account address, amount raised, + optional target address, optional embedded call, finalization status, and contributor count. + Returns `None` if the crowdloan does not exist. + + Notes: + + - Crowdloans Overview: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Crowdloan", + storage_function="Crowdloans", + params=[crowdloan_id], + block_hash=block_hash, + ) + if not query: + return None + return self._decode_crowdloan_entry( + crowdloan_id=crowdloan_id, data=query.value, block_hash=block_hash + ) + + def get_crowdloan_next_id( + self, + block: Optional[int] = None, + ) -> int: + """Retrieves the next available crowdloan identifier. + + Crowdloan IDs are allocated sequentially starting from 0. This method returns the ID that will be + assigned to the next crowdloan created via :meth:`create_crowdloan`. + + Parameters: + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The next crowdloan ID (integer) to be assigned. + + Notes: + - Crowdloans Overview: + - Crowdloan Tutorial: + """ + block_hash = self.determine_block_hash(block) + result = self.substrate.query( + module="Crowdloan", + storage_function="NextCrowdloanId", + block_hash=block_hash, + ) + return int(result.value or 0) + + def get_crowdloans( + self, + block: Optional[int] = None, + ) -> list["CrowdloanInfo"]: + """Retrieves all existing crowdloan campaigns with their metadata. + + Returns comprehensive information for all crowdloans registered on the blockchain, including + both active and finalized campaigns. + + Parameters: + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + List of `CrowdloanInfo` objects, each containing: campaign ID, creator address, creator's deposit, + minimum contribution amount, end block, funding cap, funds account address, amount raised, + optional target address, optional embedded call, finalization status, and contributor count. + Returns empty list if no crowdloans exist. + + Notes: + - Crowdloans Overview: + - Crowdloan Lifecycle: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query_map( + module="Crowdloan", + storage_function="Crowdloans", + block_hash=block_hash, + ) + + crowdloans = [] + + for c_id, value_obj in getattr(query, "records", []): + data = value_obj.value + if not data: + continue + crowdloans.append( + self._decode_crowdloan_entry( + crowdloan_id=c_id, data=data, block_hash=block_hash + ) + ) + + return crowdloans + + def get_delegate_by_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional["DelegateInfo"]: + """Retrieves detailed information about a delegate neuron (validator) based on its hotkey. This function + provides a comprehensive view of the delegate's status, including its stakes, nominators, and reward + distribution. + + Parameters: + hotkey_ss58: The `SS58` address of the delegate's hotkey. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + Detailed information about the delegate neuron, `None` if not found. + + Notes: + + - + - + """ + + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegate", + params=[hotkey_ss58], + block=block, + ) + + if not result: + return None + + return DelegateInfo.from_dict(result) + + def get_delegate_identities( + self, block: Optional[int] = None + ) -> dict[str, ChainIdentity]: + """Fetches delegate identities. + + Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method + retrieves the on-chain identity information for all delegates, including display name, legal name, web URLs, + and other metadata they have set. + + Parameters: + block: The block number to query. If `None`, queries the current chain head. + + Returns: + Dictionary mapping delegate SS58 addresses to their ChainIdentity objects. + + Notes: + - + """ + identities = self.substrate.query_map( + module="SubtensorModule", + storage_function="IdentitiesV2", + block_hash=self.determine_block_hash(block), + ) + + return { + decode_account_id(ss58_address[0]): ChainIdentity.from_dict( + decode_hex_identity_dict(identity.value), + ) + for ss58_address, identity in identities + } + + def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> float: + """ + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the + percentage of rewards that the delegate claims from its nominators' stakes. + + Parameters: + hotkey_ss58: The `SS58` address of the neuron's hotkey. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + float: The delegate take percentage. + + Notes: + - + """ + result = self.query_subtensor( + name="Delegates", + block=block, + params=[hotkey_ss58], + ) + + return u16_normalized_float(result.value) # type: ignore + + def get_delegated( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list[DelegatedInfo]: + """Retrieves delegates and their associated stakes for a given nominator coldkey. + + This method identifies all delegates (validators) that a specific coldkey has staked tokens to, along with + stake amounts and other delegation information. This is useful for account holders to understand their stake + allocations and involvement in the network's delegation and consensus mechanisms. + + Parameters: + coldkey_ss58: The SS58 address of the account's coldkey. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + List of DelegatedInfo objects containing stake amounts and delegate information. Returns empty list if no + delegations exist for the coldkey. + + Notes: + - + """ + + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[coldkey_ss58], + block=block, + ) + + if not result: + return [] + + return DelegatedInfo.list_from_dicts(result) + + def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: + """Fetches all delegates registered on the chain. + + Delegates are validators that accept stake from other TAO holders (nominators/delegators). This method + retrieves comprehensive information about all delegates including their hotkeys, total stake, nominator count, + take percentage, and other metadata. + + Parameters: + block: The block number to query. If `None`, queries the current chain head. + + Returns: + List of DelegateInfo objects containing comprehensive delegate information. Returns empty list if no + delegates are registered. + + Notes: + - + """ + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=block, + ) + if result: + return DelegateInfo.list_from_dicts(result) + else: + return [] + + def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: + """Retrieves the existential deposit amount for the Bittensor blockchain. + + The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. + Accounts with balances below this threshold can be reaped (removed) to conserve network resources and prevent + blockchain bloat from dust accounts. + + Parameters: + block: The blockchain block number for the query. + + Returns: + The existential deposit amount in RAO. + + Notes: + - + """ + result = self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=self.determine_block_hash(block), + ) + + if result is None: + raise Exception("Unable to retrieve existential deposit amount.") + + return Balance.from_rao(getattr(result, "value", 0)) + + def get_ema_tao_inflow( + self, + netuid: int, + block: Optional[int] = None, + ) -> Optional[tuple[int, Balance]]: + """Retrieves the EMA (exponential moving average) of net TAO flow for a specific subnet. + + The EMA tracks net TAO flows (staking minus unstaking) with a 30-day half-life (~86.8 day window), smoothing + out short-term fluctuations while capturing sustained staking trends. This metric determines the subnet's share + of TAO emissions under the current, flow-based model. Positive values indicate net inflow (more staking than unstaking), + negative values indicate net outflow. Subnets with negative EMA flows receive zero emissions. + + Parameters: + netuid: The unique identifier of the subnet to query. + block: The block number to query. If `None`, uses latest finalized block. + + Returns: + Tuple of (last_updated_block, ema_flow) where ema_flow is the EMA of net TAO flow in TAO units. + Returns `None` if the subnet does not exist or if querying subnet 0 (root network). + + The EMA uses a smoothing factor α ≈ 0.000003209, creating a 30-day half-life and ~86.8 day window. Only direct + stake/unstake operations count toward flows; neuron registrations and root claims are excluded. Subnet 0 (root + network) does not have an EMA TAO flow value and will return `None`. + + Notes: + - Flow-based emissions: + - EMA smoothing: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="SubtensorModule", + storage_function="SubnetEmaTaoFlow", + params=[netuid], + block_hash=block_hash, + ) + + # sn0 doesn't have EmaTaoInflow + if query is None: + return None + + block_updated, tao_bits = query.value + ema_value = int(fixed_to_float(tao_bits)) + return block_updated, Balance.from_rao(ema_value) + + def get_hotkey_owner( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the owner of the given hotkey at a specific block hash. + This function queries the blockchain for the owner of the provided hotkey. If the hotkey does not exist at the + specified block hash, it returns `None`. + + Parameters: + hotkey_ss58: The SS58 address of the hotkey. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The SS58 address of the owner if the hotkey exists, or `None` if it doesn't. + """ + hk_owner_query = self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + exists = False + if hk_owner_query: + exists = self.does_hotkey_exist(hotkey_ss58, block=block) + hotkey_owner = hk_owner_query if exists else None + return hotkey_owner + + def get_last_bonds_reset( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ): + """Retrieves the block number when bonds were last reset for a specific hotkey on a subnet. + + Parameters: + netuid: The network uid to fetch from. + hotkey_ss58: The hotkey of the neuron for which to fetch the last bonds reset. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The block number when bonds were last reset, or `None` if no bonds reset has occurred. + + Notes: + - + - + """ + return self.substrate.query( + module="Commitments", + storage_function="LastBondsReset", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + + def get_last_commitment_bonds_reset_block( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[int]: + """ + Retrieves the last block number when the bonds reset were triggered by publish_metadata for a specific neuron. + + Parameters: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + The block number when the bonds were last reset, or `None` if not found. + """ + + metagraph = self.metagraph(netuid, block=block) + try: + hotkey_ss58 = metagraph.hotkeys[uid] + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return None + block_data = self.get_last_bonds_reset(netuid, hotkey_ss58, block) + try: + return decode_block(block_data) + except TypeError: + return None + + def get_liquidity_list( + self, + wallet: "Wallet", + netuid: int, + block: Optional[int] = None, + ) -> Optional[list[LiquidityPosition]]: + """ + Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). + Calculates associated fee rewards based on current global and tick-level fee data. + + Parameters: + wallet: Wallet instance to fetch positions for. + netuid: Subnet unique id. + block: The blockchain block number for the query. + + Returns: + List of liquidity positions, or None if subnet does not exist. + """ + if not self.subnet_exists(netuid=netuid): + logging.debug(f"Subnet {netuid} does not exist.") + return None + + if not self.is_subnet_active(netuid=netuid): + logging.debug(f"Subnet {netuid} is not active.") + return None + + # Fetch positions + positions_response = self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ) + if len(positions_response.records) == 0: + return [] + + block_hash = self.determine_block_hash(block) + + # Fetch global fees and current price + fee_global_tao_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ) + fee_global_alpha_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ) + sqrt_price_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + fee_global_tao_query, fee_global_alpha_query, sqrt_price_query = ( + self.substrate.query_multi( + storage_keys=[ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], + block_hash=block_hash, + ) + ) + + fee_global_tao = fixed_to_float(fee_global_tao_query[1]) + fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) + sqrt_price = fixed_to_float(sqrt_price_query[1]) + current_tick = price_to_tick(sqrt_price**2) + + positions_values: list[tuple[dict, int, int]] = [] + positions_storage_keys: list[StorageKey] = [] + for _, p in positions_response: + position = p.value + + tick_low_idx = position["tick_low"][0] + tick_high_idx = position["tick_high"][0] + + tick_low_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ) + tick_high_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, + ) + positions_values.append((position, tick_low_idx, tick_high_idx)) + positions_storage_keys.extend([tick_low_sk, tick_high_sk]) + # query all our ticks at once + ticks_query = self.substrate.query_multi( + positions_storage_keys, block_hash=block_hash + ) + # iterator with just the values + ticks = iter([x[1] for x in ticks_query]) + positions = [] + for position, tick_low_idx, tick_high_idx in positions_values: + tick_low = next(ticks) + tick_high = next(ticks) + + # Calculate fees above/below range for both tokens + tao_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Calculate fees earned by position + fees_tao, fees_alpha = calculate_fees( + position=position, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_below, + tao_fees_above_high=tao_above, + alpha_fees_below_low=alpha_below, + alpha_fees_above_high=alpha_above, + netuid=netuid, + ) + + positions.append( + LiquidityPosition( + **{ + "id": position.get("id")[0], + "price_low": Balance.from_tao( + tick_to_price(position.get("tick_low")[0]) + ), + "price_high": Balance.from_tao( + tick_to_price(position.get("tick_high")[0]) + ), + "liquidity": Balance.from_rao(position.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + "netuid": position.get("netuid"), + } + ) + ) + + return positions + + def get_mechanism_emission_split( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[int]]: + """Returns the emission percentages allocated to each subnet mechanism. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + A list of integers representing the percentage of emission allocated to each subnet mechanism (rounded to + whole numbers). Returns None if emission is evenly split or if the data is unavailable. + """ + block_hash = self.determine_block_hash(block) + module = "SubtensorModule" + storage_function = "MechanismEmissionSplit" + if not self.substrate.get_metadata_storage_function( + module, storage_function, block_hash=block_hash + ): + return None + result = self.substrate.query( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=block_hash, + ) + if result is None or not hasattr(result, "value"): + return None + + return [round(i / sum(result.value) * 100) for i in result.value] + + def get_mechanism_count( + self, + netuid: int, + block: Optional[int] = None, + ) -> int: + """Retrieves the number of mechanisms for the given subnet. + + Parameters: + netuid: Subnet identifier. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The number of mechanisms for the given subnet. + + Notes: + - + """ + block_hash = self.determine_block_hash(block) + module = "SubtensorModule" + storage_function = "MechanismCountCurrent" + if not self.substrate.get_metadata_storage_function( + module, storage_function, block_hash=block_hash + ): + return 1 + query = self.substrate.query( + module=module, + storage_function=storage_function, + params=[netuid], + block_hash=block_hash, + ) + return query.value if query is not None and hasattr(query, "value") else 1 + + def get_metagraph_info( + self, + netuid: int, + mechid: int = 0, + selected_indices: Optional[ + Union[list[SelectiveMetagraphIndex], list[int]] + ] = None, + block: Optional[int] = None, + ) -> Optional[MetagraphInfo]: + """Retrieves full or partial metagraph information for the specified subnet (netuid). + + A metagraph is a data structure that contains comprehensive information about the current state of a subnet, + including detailed information on all the nodes (neurons) such as subnet validator stakes and subnet weights + and bonds. + + Parameters: + netuid: Subnet unique identifier. + mechid: Subnet mechanism unique identifier. + selected_indices: Optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. + If not provided, all available fields will be returned. + block: The block number at which to query the data. If `None`, queries the current chain head. + + Returns: + MetagraphInfo object with the requested subnet mechanism data, None if the subnet mechanism does not exist. + + Example: + + # Retrieve all fields from the metagraph from subnet 2 mechanism 0 + + meta_info = subtensor.get_metagraph_info(netuid=2) + + # Retrieve all fields from the metagraph from subnet 2 mechanism 1 + + meta_info = subtensor.get_metagraph_info(netuid=2, mechid=1) + + # Retrieve selective data from the metagraph from subnet 2 mechanism 0 + + partial_meta_info = subtensor.get_metagraph_info( + + netuid=2, + + selected_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + + # Retrieve selective data from the metagraph from subnet 2 mechanism 1 + + partial_meta_info = subtensor.get_metagraph_info( + netuid=2, + mechid=1, + selected_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + + Notes: + - + + """ + + block_hash: str = ( + self.determine_block_hash(block=block) or self.substrate.get_chain_head() + ) + + # Normalize selected_indices to a list of integers + if selected_indices is not None: + indexes = [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f + for f in selected_indices + ] + if 0 not in indexes: + indexes = [0] + indexes + query = self._runtime_call_with_fallback( + ( + "SubnetInfoRuntimeApi", + "get_selective_mechagraph", + [netuid, mechid, indexes], + ), + ("SubnetInfoRuntimeApi", "get_selective_metagraph", [netuid, indexes]), + block_hash=block_hash, + default_value=ValueError( + "You have specified `selected_indices` to retrieve metagraph info selectively, but the " + "selective runtime calls are not available at this block (probably too old). Do not specify " + "`selected_indices` to retrieve metagraph info selectively." + ), + ) + else: + query = self._runtime_call_with_fallback( + ( + "SubnetInfoRuntimeApi", + "get_selective_mechagraph", + [netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]], + ), + ("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]), + block_hash=block_hash, + default_value=None, + ) + + if query is None or not hasattr(query, "value") or query.value is None: + logging.error( + f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." + ) + return None + + return MetagraphInfo.from_dict(query.value) + + def get_mev_shield_current_key( + self, block: Optional[int] = None + ) -> Optional[bytes]: + """ + Retrieves the CurrentKey from the MevShield pallet storage. + + The CurrentKey contains the ML-KEM-768 public key that is currently being used for encryption in this block. + This key is rotated from NextKey at the beginning of each block. + + Parameters: + block: The blockchain block number at which to perform the query. If None, uses the current block. + + Returns: + The ML-KEM-768 public key as bytes (1184 bytes for ML-KEM-768) + + Note: + If CurrentKey is not set (None in storage), this function returns None. This can happen if no validator has + announced a key yet. + """ + block_hash = self.determine_block_hash(block=block) + query = self.substrate.query( + module="MevShield", + storage_function="CurrentKey", + block_hash=block_hash, + ) + + if query is None: + return None + + public_key_bytes = bytes(next(iter(query))) + + # Validate public_key size for ML-KEM-768 + if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MLKEM768_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + + def get_mev_shield_next_key(self, block: Optional[int] = None) -> Optional[bytes]: + """ + Retrieves the NextKey from the MevShield pallet storage. + + The NextKey contains the ML-KEM-768 public key that will be used for encryption in the next block. This key is + rotated from NextKey to CurrentKey at the beginning of each block. + + Parameters: + block: The blockchain block number at which to perform the query. If None, uses the current block. + + Returns: + The ML-KEM-768 public key as bytes (1184 bytes for ML-KEM-768) + + Note: + If NextKey is not set (None in storage), this function returns None. This can happen if no validator has + announced the next key yet. + """ + block_hash = self.determine_block_hash(block=block) + query = self.substrate.query( + module="MevShield", + storage_function="NextKey", + block_hash=block_hash, + ) + + if query is None: + return None + + public_key_bytes = bytes(next(iter(query))) + + # Validate public_key size for ML-KEM-768 (must be exactly 1184 bytes) + if len(public_key_bytes) != MLKEM768_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MLKEM768_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + + def get_mev_shield_submission( + self, + submission_id: str, + block: Optional[int] = None, + ) -> Optional[dict[str, str | int | bytes]]: + """ + Retrieves Submission from the MevShield pallet storage. + + If submission_id is provided, returns a single submission. If submission_id is None, returns all submissions from + the storage map. + + Parameters: + submission_id: The hash ID of the submission. Can be a hex string with "0x" prefix or bytes. If None, + returns all submissions. + block: The blockchain block number at which to perform the query. If None, uses the current block. + + Returns: + If submission_id is provided: A dictionary containing the submission data if found, None otherwise. The + dictionary contains: + - author: The SS58 address of the account that submitted the encrypted extrinsic + - commitment: The blake2_256 hash of the payload_core (as hex string with "0x" prefix) + - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) + - submitted_in: The block number when the submission was created + + If submission_id is None: A dictionary mapping submission IDs (as hex strings) to submission dictionaries. + + Note: + If a specific submission does not exist in storage, this function returns None. If querying all submissions + and none exist, returns an empty dictionary. + """ + block_hash = self.determine_block_hash(block=block) + submission_id = ( + submission_id[2:] if submission_id.startswith("0x") else submission_id + ) + submission_id_bytes = bytes.fromhex(submission_id) + + query = self.substrate.query( + module="MevShield", + storage_function="Submissions", + params=[submission_id_bytes], + block_hash=block_hash, + ) + + if query is None or not isinstance(query, dict): + return None + + autor = decode_account_id(query.get("author")) + commitment = bytes(query.get("commitment")[0]) + ciphertext = bytes(query.get("ciphertext")[0]) + submitted_in = query.get("submitted_in") + + return { + "author": autor, + "commitment": commitment, + "ciphertext": ciphertext, + "submitted_in": submitted_in, + } + + def get_mev_shield_submissions( + self, + block: Optional[int] = None, + ) -> Optional[dict[str, dict[str, str | int]]]: + """ + Retrieves all encrypted submissions from the MevShield pallet storage. + + This function queries the MevShield.Submissions storage map and returns all pending encrypted submissions that + have been submitted via submit_encrypted but not yet executed via execute_revealed. + + Parameters: + block: The blockchain block number for the query. If None, uses the current block. + + Returns: + A dictionary mapping wrapper_id (as hex string with "0x" prefix) to submission data dictionaries. Each + submission dictionary contains: + - author: The SS58 address of the account that submitted the encrypted extrinsic + - commitment: The blake2_256 hash of the payload_core as bytes (32 bytes) + - ciphertext: The encrypted blob as bytes (format: [u16 kem_len][kem_ct][nonce24][aead_ct]) + - submitted_in: The block number when the submission was created + + Returns None if no submissions exist in storage at the specified block. + + Note: + Submissions are automatically pruned after KEY_EPOCH_HISTORY blocks (100 blocks) by the pallet's + on_initialize hook. Only submissions that have been submitted but not yet executed will be present in + storage. + """ + block_hash = self.determine_block_hash(block=block) + query = self.substrate.query_map( + module="MevShield", + storage_function="Submissions", + block_hash=block_hash, + ) + + result = {} + for q in query: + key, value = q + value = value.value + result["0x" + bytes(key[0]).hex()] = { + "author": decode_account_id(value.get("author")), + "commitment": bytes(value.get("commitment")[0]), + "ciphertext": bytes(value.get("ciphertext")[0]), + "submitted_in": value.get("submitted_in"), + } + + return result if result else None + + def get_minimum_required_stake(self) -> Balance: + """Returns the minimum required stake threshold for nominator cleanup operations. + + This threshold is used ONLY for cleanup after unstaking operations. If a nominator's remaining stake + falls below this minimum after an unstake, the remaining stake is forcefully cleared and returned + to the coldkey to prevent dust accounts. + + This is NOT the minimum checked during staking operations. The actual minimum for staking is determined + by DefaultMinStake (typically 0.001 TAO plus fees). + + Returns: + The minimum stake threshold as a Balance object. Nominator stakes below this amount + are automatically cleared after unstake operations. + + Notes: + - + """ + result = self.substrate.query( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + + return Balance.from_rao(getattr(result, "value", 0)) + + def get_netuids_for_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> list[int]: + """Retrieves a list of subnet UIDs (netuids) where a given hotkey is a member. This function identifies the + specific subnets within the Bittensor network where the neuron associated with the hotkey is active. + + Parameters: + hotkey_ss58: The `SS58` address of the neuron's hotkey. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + A list of netuids where the neuron is a member. + + Notes: + - + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + netuids = [] + if result.records: + for record in result: + if record[1].value: + netuids.append(record[0]) + return netuids + + def get_neuron_certificate( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[Certificate]: + """ + Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a specified + subnet (netuid) of the Bittensor network. + + Parameters: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + Certificate object containing the neuron's TLS public key and algorithm, or `None` if the neuron has + not registered a certificate. + + This function is used for certificate discovery for setting up mutual tls communication between neurons. + """ + certificate_query = self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block=block, + params=[netuid, hotkey_ss58], + ) + try: + if certificate_query: + certificate = cast(dict, certificate_query) + return Certificate(certificate) + except AttributeError: + return None + return None + + def get_neuron_for_pubkey_and_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional["NeuronInfo"]: + """ + Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID + (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor + network. + + Parameters: + hotkey_ss58: The `SS58` address of the neuron's hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Optional: Detailed information about the neuron if found, `None` otherwise. + + This function is crucial for accessing specific neuron data and understanding its status, stake, and other + attributes within a particular subnet of the Bittensor ecosystem. + """ + block_hash = self.determine_block_hash(block) + uid_query = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash, + ) + if (uid := getattr(uid_query, "value", None)) is None: + return NeuronInfo.get_null_neuron() + + return self.neuron_for_uid( + uid=uid, + netuid=netuid, + block=block, + ) + + def get_next_epoch_start_block( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are determined based on the subnet's + tempo (i.e., blocks per epoch). The result is the block number at which the next epoch will begin. + + Parameters: + netuid: The unique identifier of the subnet. + block: The reference block to calculate from. If None, uses the current chain block height. + + Returns: + int: The block number at which the next epoch will start, or None if tempo is 0 or invalid. + + Notes: + - + """ + tempo = self.tempo(netuid=netuid, block=block) + current_block = block or self.block + + if not tempo: + return None + + blocks_until = self.blocks_until_next_epoch( + netuid=netuid, tempo=tempo, block=current_block + ) + + if blocks_until is None: + return None + + return current_block + blocks_until + 1 + + def get_owned_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey to query. + block: The blockchain block number for the query. + + Returns: + list[str]: A list of hotkey SS58 addresses owned by the coldkey. + """ + block_hash = self.determine_block_hash(block) + owned_hotkeys = self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + ) + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + + def get_parents( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> list[tuple[float, str]]: + """ + This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys + storage function to get the children and formats them before returning as a tuple. + + Parameters: + hotkey_ss58: The child hotkey SS58. + netuid: The netuid value. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A list of formatted parents [(proportion, parent)] + + Notes: + - + - :meth:`get_children` for retrieving child keys + """ + parents = self.substrate.query( + module="SubtensorModule", + storage_function="ParentKeys", + params=[hotkey_ss58, netuid], + block_hash=self.determine_block_hash(block), + ) + if parents: + formatted_parents = [] + for proportion, parent in parents.value: + # Convert U64 to int + formatted_child = decode_account_id(parent[0]) + normalized_proportion = u64_normalized_float(proportion) + formatted_parents.append((normalized_proportion, formatted_child)) + return formatted_parents + + return [] + + def get_proxies(self, block: Optional[int] = None) -> dict[str, list[ProxyInfo]]: + """ + Retrieves all proxy relationships from the chain. + + This method queries the Proxy.Proxies storage map across all accounts and returns a dictionary mapping each real + account (delegator) to its list of proxy relationships. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo contains the + delegate address, proxy type, and delay for that proxy relationship. + + Notes: + - This method queries all proxy relationships on the chain, which may be resource-intensive for large + networks. Consider using :meth:`get_proxies_for_real_account` for querying specific accounts. + - See: + """ + block_hash = self.determine_block_hash(block) + query_map = self.substrate.query_map( + module="Proxy", + storage_function="Proxies", + block_hash=block_hash, + ) + + proxies = {} + for record in query_map: + real_account, proxy_list = ProxyInfo.from_query_map_record(record) + proxies[real_account] = proxy_list + return proxies + + def get_proxies_for_real_account( + self, + real_account_ss58: str, + block: Optional[int] = None, + ) -> tuple[list[ProxyInfo], Balance]: + """ + Returns proxy/ies associated with the provided real account. + + This method queries the Proxy.Proxies storage for a specific real account and returns all proxy relationships + where this real account is the delegator. It also returns the deposit amount reserved for these proxies. + + Parameters: + real_account_ss58: SS58 address of the real account (delegator) whose proxies to retrieve. + block: The blockchain block number for the query. + + Returns: + Tuple containing: + - List of ProxyInfo objects representing all proxy relationships for the real account. Each ProxyInfo + contains delegate address, proxy type, and delay. + - Balance object representing the reserved deposit amount for these proxies. This deposit is held as + long as the proxy relationships exist and is returned when proxies are removed. + + Notes: + - If the account has no proxies, returns an empty list and a zero balance. + - See: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Proxy", + storage_function="Proxies", + params=[real_account_ss58], + block_hash=block_hash, + ) + return ProxyInfo.from_query(query) + + def get_proxy_announcement( + self, + delegate_account_ss58: str, + block: Optional[int] = None, + ) -> list[ProxyAnnouncementInfo]: + """ + Retrieves proxy announcements for a specific delegate account. + + This method queries the Proxy.Announcements storage for announcements made by the given delegate proxy account. + Announcements allow a proxy to declare its intention to execute a call on behalf of a real account after a delay + period. + + Parameters: + delegate_account_ss58: SS58 address of the delegate proxy account whose announcements to retrieve. + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + List of ProxyAnnouncementInfo objects. Each object contains the real account address, call hash, and block + height at which the announcement was made. + + Notes: + - If the delegate has no announcements, returns an empty list. + - See: + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Proxy", + storage_function="Announcements", + params=[delegate_account_ss58], + block_hash=block_hash, + ) + return ProxyAnnouncementInfo.from_dict(query.value[0]) + + def get_proxy_announcements( + self, + block: Optional[int] = None, + ) -> dict[str, list[ProxyAnnouncementInfo]]: + """ + Retrieves all proxy announcements from the chain. + + This method queries the Proxy.Announcements storage map across all delegate accounts and returns a dictionary + mapping each delegate to its list of pending announcements. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + Dictionary mapping delegate account SS58 addresses to lists of ProxyAnnouncementInfo objects. + Each ProxyAnnouncementInfo contains the real account address, call hash, and block height. + + Notes: + - This method queries all announcements on the chain, which may be resource-intensive for large networks. + Consider using :meth:`get_proxy_announcement` for querying specific delegates. + - See: + """ + block_hash = self.determine_block_hash(block) + query_map = self.substrate.query_map( + module="Proxy", + storage_function="Announcements", + block_hash=block_hash, + ) + announcements = {} + for record in query_map: + delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) + announcements[delegate] = proxy_list + return announcements + + def get_proxy_constants( + self, + constants: Optional[list[str]] = None, + as_dict: bool = False, + block: Optional[int] = None, + ) -> Union["ProxyConstants", dict]: + """ + Fetches runtime configuration constants from the `Proxy` pallet. + + This method retrieves on-chain configuration constants that define deposit requirements, proxy limits, and + announcement constraints for the Proxy pallet. These constants govern how proxy accounts operate within the + Subtensor network. + + Parameters: + constants: Optional list of specific constant names to fetch. If omitted, all constants defined in + `ProxyConstants.constants_names()` are queried. Valid constant names include: "AnnouncementDepositBase", + "AnnouncementDepositFactor", "MaxProxies", "MaxPending", "ProxyDepositBase", "ProxyDepositFactor". + as_dict: If True, returns the constants as a dictionary instead of a `ProxyConstants` object. + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + If `as_dict` is False: ProxyConstants object containing all requested constants. + If `as_dict` is True: Dictionary mapping constant names to their values (Balance objects for deposit + constants, integers for limit constants). + + Notes: + - All Balance amounts are returned in RAO. Constants reflect the current chain configuration at the specified + block. + - See: + """ + result = {} + const_names = constants or ProxyConstants.constants_names() + + for const_name in const_names: + query = self.query_constant( + module_name="Proxy", + constant_name=const_name, + block=block, + ) + + if query is not None: + result[const_name] = query.value + + proxy_constants = ProxyConstants.from_dict(result) + + return proxy_constants.to_dict() if as_dict else proxy_constants + + def get_revealed_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns uid related revealed commitment for a given netuid. + + Parameters: + netuid: The unique identifier of the subnetwork. + uid: The neuron uid to retrieve the commitment from. + block: The block number to retrieve the commitment from. If `None`, queries the current chain head. + + Returns: + A tuple of reveal block and commitment message. + + Example: + + # sample return value + + ( (12, "Alice message 1"), (152, "Alice message 2") ) + + ( (12, "Bob message 1"), (147, "Bob message 2") ) + + Notes: + - + """ + try: + meta_info = self.get_metagraph_info(netuid, block=block) + if meta_info: + hotkey_ss58 = meta_info.hotkeys[uid] + else: + raise ValueError(f"Subnet with netuid {netuid} does not exist.") + except IndexError: + raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") + + return self.get_revealed_commitment_by_hotkey( + netuid=netuid, hotkey_ss58=hotkey_ss58, block=block + ) + + def get_revealed_commitment_by_hotkey( + self, + netuid: int, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + # TODO: Clarify return ordering and units; add Examples + """Retrieves hotkey related revealed commitment for a given subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + hotkey_ss58: The ss58 address of the committee member. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A tuple of reveal block and commitment message. + + Notes: + - + """ + if not is_valid_ss58_address(address=hotkey_ss58): + raise ValueError(f"Invalid ss58 address {hotkey_ss58} provided.") + + query = self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58], + block=block, + ) + if query is None: + return None + return tuple(decode_revealed_commitment(pair) for pair in query) + + def get_root_claim_type( + self, + coldkey_ss58: str, + block: Optional[int] = None, + ) -> Union[str, dict]: + """Return the configured root claim type for a given coldkey. + + The root claim type controls how dividends from staking to the Root Subnet (subnet 0) are processed when they + are claimed: + + - `Swap` (default): Alpha dividends are swapped to TAO at claim time and restaked on the root subnet. + - `Keep`: Alpha dividends remain as Alpha on the originating subnets. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey whose root claim preference to query. + block: The block number to query. Do not specify if using `block_hash` or `reuse_block`. + + Returns: + + The root claim type as a string, either `Swap` or `Keep`, + or dict for "KeepSubnets" in format {"KeepSubnets": {"subnets": [1, 2, 3]}}. + + Notes: + - The claim type applies to both automatic and manual root claims; it does not affect the original TAO stake + on subnet 0, only how Alpha dividends are treated. + - See: + - See also: + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimType", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + # Query returns enum as dict: {"Swap": ()} or {"Keep": ()} or {"KeepSubnets": {"subnets": [1, 2, 3]}} + variant_name = next(iter(query.keys())) + variant_value = query[variant_name] + + # For simple variants (Swap, Keep), value is empty tuple, return string + if not variant_value or variant_value == (): + return variant_name + + # For KeepSubnets, value contains the data, return full dict structure + if isinstance(variant_value, dict) and "subnets" in variant_value: + subnets_raw = variant_value["subnets"] + subnets = list(subnets_raw[0]) + + return {variant_name: {"subnets": subnets}} + + return {variant_name: variant_value} + + def get_root_alpha_dividends_per_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Retrieves the root alpha dividends per subnet for a given hotkey. + + This storage tracks the root alpha dividends that a hotkey has received on a specific subnet. + It is updated during block emission distribution when root alpha is distributed to validators. + + Parameters: + hotkey_ss58: The ss58 address of the root validator hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Balance: The root alpha dividends for this hotkey on this subnet in Rao, with unit set to netuid. + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootAlphaDividendsPerSubnet", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return Balance.from_rao(query.value).set_unit(netuid=netuid) + + def get_root_claimable_rate( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> float: + """Return the fraction of root stake currently claimable on a subnet. + + This method returns a normalized rate representing how much Alpha dividends are currently claimable on the given + subnet relative to the validator's root stake. It is primarily a low-level helper; most users should call + :meth:`get_root_claimable_stake` instead to obtain a Balance. + + Parameters: + hotkey_ss58: The SS58 address of the root validator hotkey. + netuid: The unique identifier of the subnet whose claimable rate to compute. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + A float representing the claimable rate for this subnet (approximately in the range `[0.0, 1.0]`). A value + of 0.0 means there are currently no claimable Alpha dividends on the subnet. + + Notes: + - Use :meth:`get_root_claimable_stake` to retrieve the actual claimable amount as a `Balance` object. + - See: + """ + all_rates = self.get_root_claimable_all_rates( + hotkey_ss58=hotkey_ss58, + block=block, + ) + return all_rates.get(netuid, 0.0) + + def get_root_claimable_all_rates( + self, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> dict[int, float]: + """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. + + Parameters: + hotkey_ss58: The SS58 address of the root validator hotkey. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + Dictionary mapping `netuid` to a float claimable rate (approximately in the range `[0.0, 1.0]`) for that + subnet. Missing entries imply no claimable Alpha dividends for that subnet. + + Notes: + - See: + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + bits_list = next(iter(query.value)) + return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + + def get_root_claimable_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Return the currently claimable Alpha staking dividends for a coldkey from a root validator on a subnet. + + Parameters: + coldkey_ss58: The SS58 address of the delegator's coldkey. + hotkey_ss58: The SS58 address of the root validator hotkey. + netuid: The subnet ID where Alpha dividends will be claimed. + block: The block number to query. If `None`, queries the current chain head. + + Returns: + `Balance` representing the Alpha stake currently available to claim on the specified subnet (unit is the + subnet's Alpha token). + + Notes: + - After a successful manual or automatic claim, this value typically drops to zero for that subnet until new + dividends accumulate. + - The underlying TAO stake on the Root Subnet remains unaffected; only Alpha dividends are moved or swapped + according to the configured root claim type. + - See: + - See also: + """ + root_stake = self.get_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, # root netuid + block=block, + ) + root_claimable_rate = self.get_root_claimable_rate( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + ) + root_claimable_stake = (root_claimable_rate * root_stake).set_unit( + netuid=netuid + ) + root_claimed = self.get_root_claimed( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + block=block, + netuid=netuid, + ) + return max( + root_claimable_stake - root_claimed, Balance(0).set_unit(netuid=netuid) + ) + + def get_root_claimed( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Return the total Alpha dividends already claimed for a coldkey from a root validator on a subnet. + + Parameters: + coldkey_ss58: The SS58 address of the delegator's coldkey. + hotkey_ss58: The SS58 address of the root validator hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + `Balance` representing the cumulative Alpha stake that has already been claimed from the root validator on + the specified subnet. + + Notes: + - See: + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimed", + params=[netuid, hotkey_ss58, coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return Balance.from_rao(query.value).set_unit(netuid=netuid) + + def get_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """ + Returns the amount of Alpha staked by a specific coldkey to a specific hotkey within a given subnet. + This function retrieves the delegated stake balance, referred to as the 'Alpha' value. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey that delegated the stake. This address owns the stake. + hotkey_ss58: The ss58 address of the hotkey which the stake is on. + netuid: The unique identifier of the subnet to query. + block: The specific block number at which to retrieve the stake information. + + Returns: + An object representing the amount of Alpha (TAO ONLY if the subnet's netuid is 0) currently staked from the + specified coldkey to the specified hotkey within the given subnet. + """ + alpha_shares_query = self.query_module( + module="SubtensorModule", + name="Alpha", + block=block, + params=[hotkey_ss58, coldkey_ss58, netuid], + ) + alpha_shares = cast(FixedPoint, alpha_shares_query) + + hotkey_alpha_obj: ScaleObj = self.query_module( + module="SubtensorModule", + name="TotalHotkeyAlpha", + block=block, + params=[hotkey_ss58, netuid], + ) + hotkey_alpha = hotkey_alpha_obj.value + + hotkey_shares_query = self.query_module( + module="SubtensorModule", + name="TotalHotkeyShares", + block=block, + params=[hotkey_ss58, netuid], + ) + hotkey_shares = cast(FixedPoint, hotkey_shares_query) + + alpha_shares_as_float = fixed_to_float(alpha_shares) + hotkey_shares_as_float = fixed_to_float(hotkey_shares) + + if hotkey_shares_as_float == 0: + return Balance.from_rao(0).set_unit(netuid=netuid) + + stake = alpha_shares_as_float / hotkey_shares_as_float * hotkey_alpha + + return Balance.from_rao(int(stake)).set_unit(netuid=netuid) + + def get_stake_for_coldkey_and_hotkey( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuids: Optional[UIDs] = None, + block: Optional[int] = None, + ) -> dict[int, StakeInfo]: + """ + Retrieves all coldkey-hotkey pairing stake across specified (or all) subnets + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + hotkey_ss58: The SS58 address of the hotkey. + netuids: The subnet IDs to query for. Set to `None` for all subnets. + block: The block number at which to query the stake information. + + Returns: + A netuid to StakeInfo mapping of all stakes across all subnets. + """ + if netuids is None: + all_netuids = self.get_all_subnets_netuid(block=block) + else: + all_netuids = netuids + results = [ + self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_hotkey_coldkey_netuid", + params=[hotkey_ss58, coldkey_ss58, netuid], + block=block, + ) + for netuid in all_netuids + ] + return { + netuid: StakeInfo.from_dict(result) + for (netuid, result) in zip(all_netuids, results) + } + + def get_stake_info_for_coldkey( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list["StakeInfo"]: + """ + Retrieves the stake information for a given coldkey. + + Parameters: + coldkey_ss58: The SS58 address of the coldkey. + block: The block number at which to query the stake information. + + Returns: + List of StakeInfo objects. + """ + result = self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[coldkey_ss58], + block=block, + ) + + if result is None: + return [] + return StakeInfo.list_from_dicts(result) + + def get_stake_info_for_coldkeys( + self, coldkey_ss58s: list[str], block: Optional[int] = None + ) -> dict[str, list["StakeInfo"]]: + """ + Retrieves the stake information for multiple coldkeys. + + Parameters: + coldkey_ss58s: A list of SS58 addresses of the coldkeys to query. + block: The block number at which to query the stake information. + + Returns: + The dictionary mapping coldkey addresses to a list of StakeInfo objects. + """ + query = self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkeys", + params=[coldkey_ss58s], + block=block, + ) + + if query is None: + return {} + + return { + decode_account_id(ck): StakeInfo.list_from_dicts(st_info) + for ck, st_info in query + } + + def get_stake_for_hotkey( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Balance: + """ + Retrieves the stake information for a given hotkey. + + Parameters: + hotkey_ss58: The SS58 address of the hotkey. + netuid: The subnet ID to query for. + block: The block number at which to query the stake information. + """ + hotkey_alpha_query = self.query_subtensor( + name="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block=block + ) + hotkey_alpha = cast(ScaleObj, hotkey_alpha_query) + balance = Balance.from_rao(hotkey_alpha.value) + balance.set_unit(netuid=netuid) + return balance + + get_hotkey_stake = get_stake_for_hotkey + + def get_stake_add_fee( + self, + amount: Balance, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for adding new stake to a hotkey. + + Parameters: + amount: Amount of stake to add in TAO + netuid: Netuid of subnet + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object in TAO. + """ + check_balance_amount(amount) + sim_swap_result = self.sim_swap( + origin_netuid=0, destination_netuid=netuid, amount=amount, block=block + ) + return sim_swap_result.tao_fee + + def get_stake_movement_fee( + self, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for moving stake between hotkeys/subnets/coldkeys. + + Parameters: + origin_netuid: Netuid of source subnet. + destination_netuid: Netuid of the destination subnet. + amount: Amount of stake to move. + block: The block number for which the children are to be retrieved. + + Returns: + The calculated stake fee as a Balance object + """ + check_balance_amount(amount) + sim_swap_result = self.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=block, + ) + return sim_swap_result.tao_fee + + def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[float]: + """ + Retrieves the stake weight for all hotkeys in a given subnet. + + Parameters: + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + + Returns: + A list of stake weights for all hotkeys in the specified subnet. + """ + block_hash = self.determine_block_hash(block=block) + result = self.substrate.query( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=block_hash, + ) + return [u16_normalized_float(w) for w in result] + + def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: + """ + Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the + amount of Tao that needs to be locked or burned to establish a new subnet. + + Parameters: + block: The blockchain block number for the query. + + Returns: + int: The burn cost for subnet registration. + + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the + proliferation of subnets and ensuring their commitment to the network's long-term viability. + """ + lock_cost = self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=block, + ) + + if lock_cost is not None: + return Balance.from_rao(lock_cost) + else: + return lock_cost + + def get_subnet_hyperparameters( + self, netuid: int, block: Optional[int] = None + ) -> Optional[Union[list, "SubnetHyperparameters"]]: + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define + the operational settings and rules governing the subnet's behavior. + + Parameters: + netuid: The network UID of the subnet to query. + block: The blockchain block number for the query. + + Returns: + The subnet's hyperparameters, or `None` if not available. + + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how + they interact with the network's consensus and incentive mechanisms. + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block=block, + ) + + if not result: + return None + + return SubnetHyperparameters.from_dict(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. + + Parameters: + 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_subnet_owner_hotkey( + self, netuid: int, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the hotkey of the subnet owner for a given network UID. + + This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its + netuid. If no data is found or the query fails, the function returns `None`. + + Parameters: + netuid: The network UID of the subnet to fetch the owner's hotkey for. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The hotkey of the subnet owner if available; `None` otherwise. + """ + return self.query_subtensor( + name="SubnetOwnerHotkey", params=[netuid], block=block + ) + + def get_subnet_price( + self, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Gets the current Alpha price in TAO for the specified subnet. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The current Alpha price in TAO units for the specified subnet. + + Notes: + Subnet 0 (root network) always returns 1 TAO since it uses TAO directly rather than Alpha. + """ + # SN0 price is always 1 TAO + if netuid == 0: + return Balance.from_tao(1) + + block_hash = self.determine_block_hash(block=block) + price_rao = self.substrate.runtime_call( + api="SwapRuntimeApi", + method="current_alpha_price", + params=[netuid], + block_hash=block_hash, + ).value + return Balance.from_rao(price_rao) + + def get_subnet_prices( + self, + block: Optional[int] = None, + ) -> dict[int, Balance]: + """Gets the current Alpha price in TAO for all subnets. + + Parameters: + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + A dictionary mapping subnet unique ID (netuid) to the current Alpha price in TAO units. + + Notes: + Subnet 0 (root network) always has a price of 1 TAO since it uses TAO directly rather than Alpha. + """ + 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_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None + ) -> int: + """Retrieves the SubnetRevealPeriodEpochs hyperparameter for a specified subnet. + + This hyperparameter determines the number of epochs that must pass before a committed weight can be revealed + in the commit-reveal mechanism. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The number of epochs in the reveal period for the subnet. + + Notes: + - + + """ + return cast( + int, + self.get_hyperparameter( + param_name="RevealPeriodEpochs", block=block, netuid=netuid + ), + ) + + def get_subnet_validator_permits( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[bool]]: + """ + Retrieves the list of validator permits for a given subnet as boolean values. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + A list of boolean values representing validator permits, or None if not available. + """ + query = self.query_subtensor( + name="ValidatorPermit", + params=[netuid], + block=block, + ) + return query.value if query is not None and hasattr(query, "value") else query + + def get_timelocked_weight_commits( + self, + netuid: int, + mechid: int = 0, + block: Optional[int] = None, + ) -> list[tuple[str, int, str, int]]: + """Retrieves CRv4 (Commit-Reveal version 4) weight commit information for a specific subnet. + + This method retrieves timelocked weight commitments made by validators using the commit-reveal mechanism. + The raw byte/vector encoding from the chain is automatically parsed and converted into a structured format + via `WeightCommitInfo`. + + Parameters: + netuid: The unique identifier of the subnet. + mechid: Subnet mechanism identifier (default 0 for primary mechanism). + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + A list of commit details, where each item is a tuple containing: + + - ss58_address: The SS58 address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message (encoded commitment data). + - reveal_round: The drand round when the commitment can be revealed. + + Notes: + The list may be empty if there are no commits found. + - + """ + storage_index = get_mechid_storage_index(netuid, mechid) + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[storage_index], + block_hash=self.determine_block_hash(block=block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + + def get_timestamp(self, block: Optional[int] = None) -> datetime: + """ + Retrieves the datetime timestamp for a given block + + Parameters: + block: The blockchain block number for the query. + + Returns: + datetime object for the timestamp of the block + """ + unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value + return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + + def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: + """Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. + + Parameters: + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The total number of subnets in the network. + + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", None) + + def get_transfer_fee( + self, + wallet: "Wallet", + destination_ss58: str, + amount: Optional[Balance], + keep_alive: bool = True, + ) -> Balance: + """ + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This + function simulates the transfer to estimate the associated cost, taking into account the current network + conditions and transaction complexity. + + Parameters: + wallet: The wallet from which the transfer is initiated. + destination_ss58: The `SS58` address of the destination account. + amount: The amount of tokens to be transferred, specified as a Balance object, or in Tao or Rao units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. + + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance + object. + + Notes: + - + """ + check_balance_amount(amount) + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params( + amount, destination_ss58, keep_alive + ) + + call = self.compose_call( + call_module="Balances", + call_function=call_function, + call_params=call_params, + ) + + try: + payment_info = self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + logging.error(f":cross_mark: [red]Failed to get payment info: [/red]{e}") + payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao + + return Balance.from_rao(payment_info["partial_fee"]) + + def get_unstake_fee( + self, + netuid: int, + amount: Balance, + block: Optional[int] = None, + ) -> Balance: + """Calculates the fee for unstaking from a hotkey. + + Parameters: + netuid: The unique identifier of the subnet. + amount: Amount of stake to unstake in TAO. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + The calculated stake fee as a Balance object in Alpha. + + Notes: + - + """ + check_balance_amount(amount) + sim_swap_result = self.sim_swap( + origin_netuid=netuid, + destination_netuid=0, + amount=amount, + block=block, + ) + return sim_swap_result.alpha_fee.set_unit(netuid=netuid) + + def get_vote_data( + self, proposal_hash: str, block: Optional[int] = None + ) -> Optional["ProposalVoteData"]: + # TODO: is this all deprecated? Didn't subtensor senate stuff get removed? + """ + Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information + about how senate members have voted on the proposal. + + Parameters: + proposal_hash: The hash of the proposal for which voting data is requested. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + An object containing the proposal's voting data, or `None` if not found. + + This function is important for tracking and understanding the decision-making processes within the Bittensor + network, particularly how proposals are received and acted upon by the governing body. + """ + vote_data: dict[str, Any] = self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=self.determine_block_hash(block), + ) + + if vote_data is None: + return None + + return ProposalVoteData.from_dict(vote_data) + + def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Parameters: + hotkey_ss58: The `SS58` address of the neuron's hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + The UID of the neuron if it is registered on the subnet, `None` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and + governance activities on a particular subnet. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", result) + + def filter_netuids_by_registered_hotkeys( + self, + all_netuids: Iterable[int], + filter_for_netuids: Iterable[int], + all_hotkeys: Iterable["Wallet"], + block: Optional[int], + ) -> list[int]: + """ + Filters netuids by combining netuids from all_netuids and netuids with registered hotkeys. + + If filter_for_netuids is empty/None: + Returns all netuids where hotkeys from all_hotkeys are registered. + + If filter_for_netuids is provided: + Returns the union of: + - Netuids from all_netuids that are in filter_for_netuids, AND + - Netuids with registered hotkeys that are in filter_for_netuids + + This allows you to get netuids that are either in your specified list (all_netuids) or have registered hotkeys, + as long as they match filter_for_netuids. + + Parameters: + all_netuids (Iterable[int]): A list of netuids to consider for filtering. + filter_for_netuids (Iterable[int]): A subset of netuids to restrict the result to. If None/empty, returns + all netuids with registered hotkeys. + all_hotkeys (Iterable[Wallet]): Hotkeys to check for registration. + block (Optional[int]): The blockchain block number for the query. + + Returns: + The filtered list of netuids (union of filtered all_netuids and registered hotkeys). + """ + self._get_block_hash(block) # just used to cache the block hash + netuids_with_registered_hotkeys = [ + item + for sublist in [ + self.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, + block=block, + ) + for wallet in all_hotkeys + ] + for item in sublist + ] + + if not filter_for_netuids: + all_netuids = netuids_with_registered_hotkeys + + else: + filtered_netuids = [ + netuid for netuid in all_netuids if netuid in filter_for_netuids + ] + + registered_hotkeys_filtered = [ + netuid + for netuid in netuids_with_registered_hotkeys + if netuid in filter_for_netuids + ] + + # Combine both filtered lists + all_netuids = filtered_netuids + registered_hotkeys_filtered + + return list(set(all_netuids)) + + def immunity_period( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during + which new neurons are protected from certain network penalties or restrictions. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, `None` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants + have a grace period to establish themselves and contribute to the network without facing immediate punitive + actions. + """ + call = self.get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def is_in_admin_freeze_window( + self, + netuid: int, + block: Optional[int] = None, + ) -> bool: + """ + Returns True if the current block is within the terminal freeze window of the tempo + for the given subnet. During this window, admin ops are prohibited to avoid interference + with validator weight submissions. + + Parameters: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + bool: True if in freeze window, else False. + """ + # SN0 doesn't have admin_freeze_window + if netuid == 0: + return False + + next_epoch_start_block = self.get_next_epoch_start_block( + netuid=netuid, block=block + ) + + if next_epoch_start_block is not None: + remaining = next_epoch_start_block - self.block + window = self.get_admin_freeze_window(block=block) + return remaining < window + return False + + def is_fast_blocks(self) -> bool: + """Checks if the node is running with fast blocks enabled. + + Fast blocks have a block time of 10 seconds, compared to the standard 12-second block time. This affects + transaction timing and network synchronization. + + Returns: + `True` if fast blocks are enabled (10-second block time), `False` otherwise (12-second block time). + + Notes: + - + + """ + return self.query_constant("SubtensorModule", "DurationOfStartCall") == 10 + + def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + """ + Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if + the neuron associated with the hotkey is part of the network's delegation system. + + Parameters: + hotkey_ss58: The SS58 address of the neuron's hotkey. + block: The blockchain block number for the query. + + Returns: + `True` if the hotkey is a delegate, `False` otherwise. + + Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in + consensus and governance processes. + """ + delegates = self.get_delegates(block) + return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] + + def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across + any subnet or specifically on a specified subnet. This function checks the registration status of a neuron + identified by its hotkey, which is crucial for validating its participation and activities within the network. + + Parameters: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet to check the registration. If `None`, the registration is + checked across all subnets. + block: The blockchain block number at which to perform the query. + + Returns: + `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), + `False` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in + understanding whether a neuron is eligible to participate in network processes such as consensus, validation, + and incentive distribution based on its registration status. + """ + if netuid is None: + return self.is_hotkey_registered_any(hotkey_ss58, block) + else: + return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + + def is_hotkey_registered_any( + self, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Parameters: + hotkey_ss58: The `SS58` address of the neuron's hotkey. + block: The blockchain block number for the query. + + Returns: + `True` if the hotkey is registered on any subnet, False otherwise. + """ + hotkeys = self.get_netuids_for_hotkey(hotkey_ss58, block) + return len(hotkeys) > 0 + + def is_hotkey_registered_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> bool: + """Checks if the hotkey is registered on a given subnet. + + Parameters: + hotkey_ss58: The SS58 address of the hotkey to check. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + `True` if the hotkey is registered on the specified subnet, `False` otherwise. + + Notes: + - + + """ + return ( + self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block) + is not None + ) + + def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: + """Verifies if a subnet with the provided netuid is active. + + A subnet is considered active if the `start_call` extrinsic has been executed. A newly registered subnet + may exist but not be active until the subnet owner calls `start_call` to begin emissions. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. If `None`, queries the current chain head. + + Returns: + `True` if the subnet is active (emissions have started), `False` otherwise. + + Notes: + - + + """ + query = self.query_subtensor( + name="FirstEmissionBlockNumber", + block=block, + params=[netuid], + ) + return True if query and query.value > 0 else False + + def last_drand_round(self) -> Optional[int]: + """Retrieves the last drand round emitted in Bittensor. + + Drand (distributed randomness) rounds are used to determine when committed weights can be revealed in the + commit-reveal mechanism. This method returns the most recent drand round number, which corresponds to the + timing for weight reveals. + + Returns: + The latest drand round number emitted in Bittensor, or `None` if no round has been stored. + + Notes: + - + + """ + result = self.substrate.query( + module="Drand", storage_function="LastStoredRound" + ) + return getattr(result, "value", None) + + def max_weight_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[float]: + """Returns the MaxWeightsLimit hyperparameter for a subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + The stored maximum weight limit as a normalized float in [0, 1], or `None` if the subnetwork + does not exist. Note: this value is not actually enforced - the weight validation code uses + a hardcoded u16::MAX instead. + + Notes: + - This hyperparameter is now a constant rather than a settable variable. + - + """ + call = self.get_hyperparameter( + param_name="MaxWeightsLimit", netuid=netuid, block=block + ) + return None if call is None else u16_normalized_float(int(call)) + + def metagraph( + self, + netuid: int, + mechid: int = 0, + lite: bool = True, + block: Optional[int] = None, + ) -> "Metagraph": + """ + Returns a synced metagraph for a specified subnet within the Bittensor network. + The metagraph represents the network's structure, including neuron connections and interactions. + + Parameters: + netuid: The network UID of the subnet to query. + mechid: Subnet mechanism identifier. + lite: If `True`, returns a metagraph using a lightweight sync (no weights, no bonds). + block: Block number for synchronization, or `None` for the latest block. + + Returns: + The metagraph representing the subnet's structure and neuron relationships. + + The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's + decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. + """ + metagraph = Metagraph( + netuid=netuid, + mechid=mechid, + network=self.chain_endpoint, + lite=lite, + sync=False, + subtensor=self, + ) + metagraph.sync(block=block, lite=lite, subtensor=self) + + return metagraph + + def min_allowed_weights( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Returns the MinAllowedWeights hyperparameter for a subnet. + + This hyperparameter sets the minimum length of the weights vector that a validator must submit. + It checks `weights.len() >= MinAllowedWeights`. For example, a validator could submit `[1000, 0, 0, 0]` + to satisfy `MinAllowedWeights=4`, but this would fail if `MinAllowedWeights` were set to 5. + This ensures validators distribute attention across the subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + The minimum number of required weight connections, or `None` if the subnetwork does not + exist or the parameter is not found. + + Notes: + - + """ + call = self.get_hyperparameter( + param_name="MinAllowedWeights", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def neuron_for_uid( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> "NeuronInfo": + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's + attributes, including its stake, rank, and operational status. + + Parameters: + uid: The unique identifier of the neuron. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Detailed information about the neuron if found, a null neuron otherwise + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, + offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], + block=block, + ) + + if not result: + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_dict(result) + + def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: + """ + Retrieves a list of all neurons within a specified subnet of the Bittensor network. + This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and + network interactions. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's + decentralized structure and the dynamics of its consensus and governance processes. + """ + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[netuid], + block=block, + ) + + if not result: + return [] + + return NeuronInfo.list_from_dicts(result) + + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list["NeuronInfoLite"]: + """ + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. + This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network + participation. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis + of the network's decentralized structure and neuron dynamics. + """ + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[netuid], + block=block, + ) + + if not result: + return [] + + return NeuronInfoLite.list_from_dicts(result) + + def query_identity( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> Optional[ChainIdentity]: + """ + Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves + detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized + identity and governance system. + + Parameters: + coldkey_ss58: Coldkey used to query the neuron's identity (technically the neuron's coldkey SS58 address). + block: The blockchain block number for the query. + + Returns: + An object containing the identity information of the neuron if found, `None` otherwise. + + The identity information can include various attributes such as the neuron's stake, rank, and other + network-specific details, providing insights into the neuron's role and status within the Bittensor network. + + Note: + See the `Bittensor CLI documentation `_ for supported identity + parameters. + """ + identity_info = cast( + dict, + self.substrate.query( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ), + ) + + if not identity_info: + return None + + try: + return ChainIdentity.from_dict( + decode_hex_identity_dict(identity_info), + ) + except TypeError: + return None + + def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: + """Retrieves the 'Burn' hyperparameter for a specified subnet. + + The 'Burn' parameter represents the amount of TAO that is recycled when registering a neuron + on this subnet. Recycled tokens are removed from circulation but can be re-emitted, unlike + burned tokens which are permanently removed. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + The amount of TAO recycled per neuron registration, or `None` if the subnet does not exist. + + Notes: + - + """ + call = self.get_hyperparameter(param_name="Burn", netuid=netuid, block=block) + return None if call is None else Balance.from_rao(int(call)) + + def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicInfo]: + """ + Retrieves the subnet information for a single subnet in the network. + + Parameters: + netuid: The unique identifier of the subnet. + block: The block number to query the subnet information from. + + Returns: + A DynamicInfo object, containing detailed information about a subnet. + """ + block_hash = self.determine_block_hash(block=block) + + query = self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ) + + if isinstance(decoded := query.decode(), dict): + try: + price = self.get_subnet_price(netuid=netuid, block=block) + except (SubstrateRequestException, ValueError): + price = None + return DynamicInfo.from_dict({**decoded, "price": price}) + return None + + def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: + """ + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + `True` if the subnet exists, `False` otherwise. + + This function is critical for verifying the presence of specific subnets in the network, enabling a deeper + understanding of the network's structure and composition. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", False) + + def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """Returns the current number of registered neurons (UIDs) in a subnet. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + The current number of registered neurons in the subnet, or `None` if the subnetwork does not exist. + + """ + call = self.get_hyperparameter( + param_name="SubnetworkN", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """Returns the Tempo hyperparameter for a subnet. + + Tempo determines the length of an epoch in blocks. It defines how frequently the subnet's consensus mechanism + runs, calculating emissions and updating rankings. A tempo of 360 blocks equals approximately 72 minutes + (360 blocks × 12 seconds per block). + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + The tempo value in blocks, or `None` if the subnetwork does not exist. + + Notes: + - + - + """ + call = self.get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) + return None if call is None else int(call) + + def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: + """ + Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. + This rate limit sets the maximum number of transactions that can be processed within a given time frame. + + Parameters: + block: The blockchain block number for the query. + + Returns: + The transaction rate limit of the network, None if not available. + + The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor + network. It helps in managing network load and preventing congestion, thereby maintaining efficient and + timely transaction processing. + """ + result = self.query_subtensor("TxRateLimit", block=block) + return getattr(result, "value", None) + + def wait_for_block(self, block: Optional[int] = None): + """ + Waits until a specific block is reached on the chain. If no block is specified, + waits for the next block. + + Parameters: + block: The block number to wait for. If None, waits for the next block. + + Returns: + True if the target block was reached, False if timeout occurred. + + Example: + + # Waits for a specific block + + subtensor.wait_for_block(block=1234) + """ + + def handler(block_data: dict): + logging.debug( + f"reached block {block_data['header']['number']}. Waiting for block {target_block}" + ) + if block_data["header"]["number"] >= target_block: + return True + return None + + current_block = self.substrate.get_block() + current_block_hash = current_block.get("header", {}).get("hash") + if block is not None: + target_block = block + else: + target_block = current_block["header"]["number"] + 1 + + self.substrate.get_block_handler( + current_block_hash, header_only=True, subscription_handler=handler + ) + return True + + def weights( + self, + netuid: int, + mechid: int = 0, + block: Optional[int] = None, + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. + This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust + and value assignment mechanisms. + + Parameters: + netuid: The network UID of the subnet to query. + mechid: Subnet mechanism identifier. + block: Block number for synchronization, or `None` for the latest block. + + Returns: + A list of tuples mapping each neuron's UID to its assigned weights. + + The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, + influencing their influence and reward allocation within the subnet. + """ + storage_index = get_mechid_storage_index(netuid, mechid) + w_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Weights", + params=[storage_index], + block_hash=self.determine_block_hash(block), + ) + w_map = [(uid, w.value or []) for uid, w in w_map_encoded] + + return w_map + + def weights_rate_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Returns the WeightsSetRateLimit hyperparameter for a subnet. + + This hyperparameter limits how many times a validator can set weights per epoch. It prevents validators + from spamming weight updates and ensures stable consensus calculations. Once the limit is reached, validators + must wait until the next epoch to set weights again. + + Parameters: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + The maximum number of weight set operations allowed per epoch, or `None` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="WeightsSetRateLimit", netuid=netuid, block=block + ) + return None if call is None else int(call) + + # Extrinsics helpers =============================================================================================== + + def validate_extrinsic_params( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + ): + """ + Validate and filter extrinsic parameters against on-chain metadata. + + This method checks that the provided parameters match the expected signature of the given extrinsic (module and + function) as defined in the Substrate metadata. It raises explicit errors for missing or invalid parameters and + silently ignores any extra keys not present in the function definition. + + Parameters: + call_module: The pallet name, e.g. "SubtensorModule" or "AdminUtils". + call_function: The extrinsic function name, e.g. "set_weights" or "sudo_set_tempo". + call_params: A dictionary of parameters to validate. + block: Optional block number to query metadata from. If not provided, the latest metadata is used. + + Returns: + A filtered dictionary containing only the parameters that are valid for the specified extrinsic. + + Raises: + ValueError: If the given module or function is not found in the chain metadata. + KeyError: If one or more required parameters are missing. + + Notes: + This method does not compose or submit the extrinsic. It only ensures that `call_params` conforms to the + expected schema derived from on-chain metadata. + """ + block_hash = self.determine_block_hash(block=block) + + func_meta = self.substrate.get_metadata_call_function( + module_name=call_module, + call_function_name=call_function, + block_hash=block_hash, + ) + + if not func_meta: + raise ValueError( + f"Call {call_module}.{call_function} not found in chain metadata." + ) + + # Expected params from metadata + expected_params = func_meta.get_param_info() + provided_params = {} + + # Validate and filter parameters + for param_name in expected_params.keys(): + if param_name not in call_params: + raise KeyError(f"Missing required parameter: '{param_name}'") + provided_params[param_name] = call_params[param_name] + + # Warn about extra params not defined in metadata + extra_params = set(call_params.keys()) - set(expected_params.keys()) + if extra_params: + logging.debug( + f"Ignoring extra parameters for {call_module}.{call_function}: {extra_params}." + ) + return provided_params + + def compose_call( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + ) -> "GenericCall": + """ + Dynamically compose a GenericCall using on-chain Substrate metadata after validating the provided parameters. + + Parameters: + call_module: Pallet name (e.g. "SubtensorModule", "AdminUtils"). + call_function: Function name (e.g. "set_weights", "sudo_set_tempo"). + call_params: Dictionary of parameters for the call. + block: Block number for querying metadata. + + Returns: + GenericCall: Composed call object ready for extrinsic submission. + + Notes: + For detailed documentation and examples of composing calls, including the CallBuilder utility, see: + + """ + call_params = self.validate_extrinsic_params( + call_module, call_function, call_params, block + ) + block_hash = self.determine_block_hash(block=block) + logging.debug( + f"Composing GenericCall -> {call_module}.{call_function} " + f"with params: {call_params}." + ) + return self.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + block_hash=block_hash, + ) + + def sign_and_send_extrinsic( + self, + call: "GenericCall", + wallet: "Wallet", + sign_with: str = "coldkey", + use_nonce: bool = False, + nonce_key: str = "hotkey", + nonce: Optional[int] = None, + *, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + calling_function: Optional[str] = None, + ) -> ExtrinsicResponse: + """ + Helper method to sign and submit an extrinsic call to chain. + + Parameters: + call: A prepared Call object + wallet: The wallet whose coldkey will be used to sign the extrinsic + sign_with: The wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + use_nonce: Unique identifier for the transaction related with hot/coldkey. + nonce_key: The type on nonce to use. Options are "hotkey" or "coldkey". + nonce: The nonce to use for the transaction. If not provided, it will be fetched from the chain. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises the relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait until the extrinsic call is included on the chain + wait_for_finalization: Whether to wait until the extrinsic call is finalized on the chain + calling_function: The name of the calling function. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Raises: + SubstrateRequestException: Substrate request exception. + """ + extrinsic_response = ExtrinsicResponse( + extrinsic_function=calling_function + if calling_function + else get_caller_name() + ) + possible_keys = ("coldkey", "hotkey", "coldkeypub") + if sign_with not in possible_keys: + raise AttributeError( + f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" + ) + + signing_keypair = getattr(wallet, sign_with) + extrinsic_data = {"call": call, "keypair": signing_keypair} + + if nonce is not None: + extrinsic_data["nonce"] = nonce + elif use_nonce: + if nonce_key not in possible_keys: + raise AttributeError( + f"'nonce_key' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{nonce_key}'" + ) + next_nonce = self.substrate.get_account_next_index( + getattr(wallet, nonce_key).ss58_address + ) + extrinsic_data["nonce"] = next_nonce + + if period is not None: + extrinsic_data["era"] = {"period": period} + + extrinsic_response.extrinsic = self.substrate.create_signed_extrinsic( + **extrinsic_data + ) + try: + response = self.substrate.submit_extrinsic( + extrinsic=extrinsic_response.extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + extrinsic_response.extrinsic_fee = self.get_extrinsic_fee( + call=call, keypair=signing_keypair + ) + extrinsic_response.message = ( + "Not waiting for finalization or inclusion." + ) + logging.debug(extrinsic_response.message) + return extrinsic_response + + extrinsic_response.extrinsic_receipt = response + + if response.is_success: + extrinsic_response.extrinsic_fee = Balance.from_rao( + response.total_fee_amount + ) + extrinsic_response.message = "Success" + return extrinsic_response + + response_error_message = response.error_message + + if raise_error: + raise ChainError.from_error(response_error_message) + + extrinsic_response.success = False + extrinsic_response.message = format_error_message(response_error_message) + extrinsic_response.error = response_error_message + return extrinsic_response + + except SubstrateRequestException as error: + if raise_error: + raise + + extrinsic_response.success = False + extrinsic_response.message = format_error_message(error) + extrinsic_response.error = error + return extrinsic_response + + def get_extrinsic_fee( + self, + call: "GenericCall", + keypair: "Keypair", + ) -> Balance: + """Gets the extrinsic fee for a given extrinsic call and keypair. + + This method estimates the transaction fee that will be charged for submitting the extrinsic to the + blockchain. The fee is returned in Rao (the smallest unit of TAO, where 1 TAO = 1e9 Rao). + + Parameters: + call: The extrinsic GenericCall object representing the transaction to estimate. + keypair: The keypair associated with the extrinsic (used to determine the account paying the fee). + + Returns: + Balance object representing the extrinsic fee in Rao. + + Example: + + # Estimate fee before sending a transfer + + call = subtensor.compose_call( + + call_module="Balances", + + call_function="transfer", + + call_params={"dest": destination_ss58, "value": amount.rao} + + ) + + fee = subtensor.get_extrinsic_fee(call=call, keypair=wallet.coldkey) + + print(f"Estimated fee: {fee.tao} TAO") + + Notes: + To create the GenericCall object, use the `compose_call` method with proper parameters. + - + + """ + payment_info = self.substrate.get_payment_info(call=call, keypair=keypair) + return Balance.from_rao(amount=payment_info["partial_fee"]) + + # Extrinsics ======================================================================================================= + + def add_stake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Adds stake from the specified wallet to a neuron on a specified subnet. + + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively + and earn incentives. This method transfers TAO from the coldkey to stake on a hotkey in a specific subnet, + converting it to Alpha (subnet-specific token) in the process. + + Parameters: + wallet: The wallet to be used for staking. + netuid: The unique identifier of the subnet to which the neuron belongs. + hotkey_ss58: The `SS58` address of the hotkey account to stake to. + amount: The amount of TAO to stake. + safe_staking: If `True`, enables price safety checks to protect against fluctuating prices. The stake will + only execute if the price change doesn't exceed the rate tolerance. + allow_partial_stake: If `True` and safe_staking is enabled, allows partial staking when the full amount would + exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. + rate_tolerance: The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price + increase. Only used when safe_staking is True. + mev_protection: If `True`, encrypts and submits the staking transaction through the MEV Shield pallet to + protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until + validators decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection + used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + When safe_staking is enabled, it provides protection against price fluctuations during the time between when + stake is submitted and when it is actually processed by the chain. + + - + - Price Protection: + - Rate Limits: + """ + check_balance_amount(amount) + return add_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def add_liquidity( + self, + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + hotkey_ss58: Optional[str] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Adds liquidity to the specified price range. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. In TAO. + price_high: The upper bound of the price tick range. In TAO. + hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + method to enable/disable user liquidity. + """ + return add_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + hotkey_ss58=hotkey_ss58, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def add_stake_multiple( + self, + wallet: "Wallet", + netuids: UIDs, + hotkey_ss58s: list[str], + amounts: list[Balance], + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Adds stakes to multiple neurons identified by their hotkey SS58 addresses. + This bulk operation allows for efficient staking across different neurons from a single wallet. + + Parameters: + wallet: The wallet used for staking. + netuids: List of subnet UIDs. + hotkey_ss58s: List of `SS58` addresses of hotkeys to stake to. + amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Price Protection: + - Rate Limits: + """ + return add_stake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def add_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Adds a proxy relationship. + + This method creates a proxy relationship where the delegate can execute calls on behalf of the real account (the + wallet owner) with restrictions defined by the proxy type and a delay period. A deposit is required and held as + long as the proxy relationship exists. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. + delay: The number of blocks before the proxy can be used. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - A deposit is required when adding a proxy. The deposit amount is determined by runtime constants and is + returned when the proxy is removed. Use :meth:`get_proxy_constants` to check current deposit requirements. + - See: + """ + return add_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def announce_proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + This method allows a proxy account to declare its intention to execute a specific call on behalf of a real + account after a delay period. The real account can review and either approve (via :meth:`proxy_announced`) or reject + (via :meth:`reject_proxy_announcement`) the announcement. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - A deposit is required when making an announcement. The deposit is returned when the announcement is + executed, rejected, or removed. The announcement can be executed after the delay period has passed. + - See: + """ + return announce_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def burned_register( + self, + wallet: "Wallet", + netuid: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling + TAO tokens, allowing them to be re-mined by performing work on the network. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + return burned_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def claim_root( + self, + wallet: "Wallet", + netuids: "UIDs", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ): + """Submit an extrinsic to manually claim accumulated root dividends from one or more subnets. + + Parameters: + wallet: Bittensor `Wallet` instance. + netuids: Iterable of subnet IDs to claim from in this call (the chain enforces a maximum number per + transaction). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: Number of blocks during which the transaction remains valid after submission. If the extrinsic is + not included in a block within this window, it will expire and be rejected. + raise_error: Whether to raise a Python exception instead of returning a failed `ExtrinsicResponse`. + wait_for_inclusion: Whether to wait until the extrinsic is included in a block before returning. + wait_for_finalization: Whether to wait for finalization of the extrinsic in a block before returning. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` describing the result of the extrinsic execution. + + Notes: + - Only Alpha dividends are claimed; the underlying TAO stake on the Root Subnet remains unchanged. + - The current root claim type (`Swap` or `Keep`) determines whether claimed Alpha is converted to + TAO and restaked on root or left as Alpha on the originating subnets. + - See: + - See also: + - Transaction fees: + """ + return claim_root_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: Salt, + uids: UIDs, + weights: Weights, + mechid: int = 0, + version_key: int = version_as_int, + max_attempts: int = 5, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = 16, + raise_error: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Parameters: + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + salt: list of randomly generated integers as salt to generated weighted hash. + uids: NumPy array of neuron UIDs for which weights are being committed. + weights: NumPy array of weight values corresponding to each UID. + mechid: Subnet mechanism unique identifier. + version_key: Version key for compatibility with the network. + max_attempts: The number of maximum attempts to commit weights. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in + time, enhancing transparency and accountability within the Bittensor network. + + Notes: + - Rate Limits: + """ + attempt = 0 + response = ExtrinsicResponse(False) + + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + + logging.debug( + f"Committing weights with params: " + f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " + f"version_key=[blue]{version_key}[/blue]" + ) + + while attempt < max_attempts and response.success is False: + try: + response = commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + except Exception as error: + return ExtrinsicResponse.from_exception( + raise_error=raise_error, error=error + ) + attempt += 1 + + if not response.success: + logging.debug( + "No one successful attempt made. " + "Perhaps it is too soon to commit weights!" + ) + return response + + def contribute_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + amount: "Balance", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Contributes TAO to an active crowdloan campaign. + + Contributions must occur before the crowdloan's end block and are subject to minimum contribution + requirements. If a contribution would push the total raised above the cap, it is automatically clipped + to fit the remaining amount. Once the cap is reached, further contributions are rejected. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (coldkey pays, coldkey receives emissions). + crowdloan_id: The unique identifier of the crowdloan to contribute to. + amount: Amount to contribute (TAO). Must meet or exceed the campaign's `min_contribution`. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Contributions can be withdrawn before finalization via `withdraw_crowdloan`. + - If the campaign does not reach its cap by the end block, contributors can be refunded via `refund_crowdloan`. + - Contributions are counted toward `MaxContributors` limit per crowdloan. + + - Crowdloans Overview: + - Crowdloan Tutorial: + """ + return contribute_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + amount=amount, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def create_crowdloan( + self, + wallet: "Wallet", + deposit: "Balance", + min_contribution: "Balance", + cap: "Balance", + end: int, + call: Optional["GenericCall"] = None, + target_address: Optional[str] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Creates a new crowdloan campaign on-chain. + + Parameters: + wallet: Bittensor Wallet instance used to sign the transaction. + deposit: Initial deposit in RAO from the creator. + min_contribution: Minimum contribution amount. + cap: Maximum cap to be raised. + end: Block number when the campaign ends. + call: Runtime call data (e.g., subtensor::register_leased_network). + target_address: SS58 address to transfer funds to on success. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure. On success, the crowdloan ID can be extracted from the + `Crowdloan.Created` event in the response. + + Notes: + - Creator cannot update `call` or `target_address` after creation. + - Creator can update `cap`, `end`, and `min_contribution` before finalization via `update_*` methods. + - Use `get_crowdloan_next_id` to determine the ID that will be assigned to the new crowdloan. + + - Crowdloans Overview: + - Crowdloan Tutorial: + - Leasing: + """ + return create_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + deposit=deposit, + min_contribution=min_contribution, + cap=cap, + end=end, + call=call, + target_address=target_address, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def create_pure_proxy( + self, + wallet: "Wallet", + proxy_type: Union[str, "ProxyType"], + delay: int, + index: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Creates a pure proxy account. + + A pure proxy is a keyless account that can only be controlled through proxy relationships. Unlike regular + proxies, pure proxies do not have their own private keys, making them more secure for certain use cases. The + pure proxy address is deterministically generated based on the spawner account, proxy type, delay, and index. + + Parameters: + wallet: Bittensor wallet object. + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. For + available proxy types and their permissions, see the documentation link in the Notes section below. + delay: Optionally, include a delay in blocks. The number of blocks that must elapse between announcing and + executing a proxied transaction. A delay of `0` means the pure proxy can be used immediately without any + announcement period. A non-zero delay creates a time-lock, requiring announcements before execution to give + the spawner time to review/reject. + index: A salt value (u16, range `0-65535`) used to generate unique pure proxy addresses. This should generally + be left as `0` unless you are creating batches of proxies. When creating multiple pure proxies with + identical parameters (same `proxy_type` and `delay`), different index values will produce different SS58 + addresses. This is not a sequential counter—you can use any unique values (e.g., 0, 100, 7, 42) in any + order. The index must be preserved as it's required for :meth:`kill_pure_proxy`. If creating multiple pure + proxies in a single batch transaction, each must have a unique index value. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The pure proxy account address can be extracted from the "PureCreated" event in the response. Store the + spawner address, proxy_type, index, height, and ext_index as they are required to kill the pure proxy later + via :meth:`kill_pure_proxy`. + - Bittensor proxies: + - Polkadot proxy documentation: + """ + return create_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def dissolve_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Dissolves a failed or refunded crowdloan, cleaning up storage and returning the creator's deposit. + + This permanently removes the crowdloan from on-chain storage and returns the creator's deposit. Can only + be called by the creator after all non-creator contributors have been refunded via `refund_crowdloan`. + This is the final step in the lifecycle of a failed crowdloan (one that did not reach its cap by the end + block). + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). + crowdloan_id: The unique identifier of the crowdloan to dissolve. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Only the creator can dissolve their own crowdloan. + - All non-creator contributors must be refunded first via `refund_crowdloan`. + - The creator's deposit (and any remaining contribution above deposit) is returned. + - After dissolution, the crowdloan is permanently removed from chain storage. + + - + """ + return dissolve_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def finalize_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Finalizes a successful crowdloan after the cap is fully raised and the end block has passed. + + Finalization executes the stored call (e.g., `register_leased_network`) or transfers raised funds to + the target address. For subnet lease crowdloans, this registers the subnet, creates a + `SubnetLeaseBeneficiary` proxy for the creator, and records contributor shares for pro-rata emissions + distribution. Leftover funds (after registration and proxy costs) are refunded to contributors. + + Only the creator can finalize, and finalization can only occur after both the end block is reached and + the total raised equals the cap. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). + crowdloan_id: The unique identifier of the crowdloan to finalize. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure. On success, a subnet lease is created (if applicable) + and contributor shares are recorded for emissions. + + Notes: + - Only the creator can finalize. + - Finalization requires `raised == cap` and `current_block >= end`. + - For subnet leases, emissions are swapped to TAO and distributed to contributors' coldkeys during the lease. + - Leftover cap (after subnet lock + proxy deposit) is refunded to contributors pro-rata. + + - Crowdloans Overview: + - Crowdloan Tutorial: + - Emissions Distribution: + """ + return finalize_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def kill_pure_proxy( + self, + wallet: "Wallet", + pure_proxy_ss58: str, + spawner: str, + proxy_type: Union[str, "ProxyType"], + index: int, + height: int, + ext_index: int, + force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Kills (removes) a pure proxy account. + + This method removes a pure proxy account that was previously created via :meth:`create_pure_proxy`. The `kill_pure` + call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This + method automatically handles this by executing the call via :meth:`proxy`. + + Parameters: + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via :meth:`create_pure_proxy`). The spawner must have an "Any" proxy relationship + with the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was + returned in the :meth:`create_pure_proxy` response. + spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via + :meth:`create_pure_proxy`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the + proxy_type used when creating the pure proxy. + index: The salt value (u16, range `0-65535`) originally used in :meth:`create_pure_proxy` to generate this + pure proxy's address. This value, combined with `proxy_type`, `delay`, and `spawner`, uniquely + identifies the pure proxy to be killed. Must match exactly the index used during creation. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. + Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the + pure proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the + spawner and the pure proxy account should be used. The spawner must have a proxy relationship of this + type (or `Any`) with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If + `None`, Substrate will automatically select an available proxy type from the spawner's proxy + relationships. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as + an `Any` proxy. This method automatically handles this by executing the call via :meth:`proxy`. The spawner + must have an `Any` proxy relationship with the pure proxy for this to work. + - See: + + Warning: + All access to this account will be lost. Any funds remaining in the pure proxy account will become + permanently inaccessible after this operation. + """ + return kill_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + force_proxy_type=force_proxy_type, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def mev_submit_encrypted( + self, + wallet: "Wallet", + call: "GenericCall", + sign_with: str = "coldkey", + *, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + blocks_for_revealed_execution: int = 3, + ) -> ExtrinsicResponse: + """ + Submits an encrypted extrinsic to the MEV Shield pallet. + + This function encrypts a call using ML-KEM-768 + XChaCha20Poly1305 and submits it to the MevShield pallet. The + extrinsic remains encrypted in the transaction pool until it is included in a block and decrypted by validators. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked, coldkey will be used for signing). + call: The GenericCall object to encrypt and submit. + sign_with: The keypair to use for signing the inner call/extrinsic. Can be either "coldkey" or "hotkey". + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the executed event, indicating that validators + have successfully decrypted and executed the inner call. If True, the function will poll subsequent + blocks for the event matching this submission's commitment. + blocks_for_revealed_execution: Maximum number of blocks to poll for the executed event after inclusion. The + function checks blocks from start_block+1 to start_block + blocks_for_revealed_execution. Returns + immediately if the event is found before the block limit is reached. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Raises: + ValueError: If NextKey is not available in storage or encryption fails. + SubstrateRequestException: If the extrinsic fails to be submitted or included. + + Note: + The encryption uses the public key from NextKey storage, which rotates every block. The payload structure is: + payload_core = signer_bytes (32B) + nonce (u32 LE, 4B) + SCALE(call) + plaintext = payload_core + b"\\x01" + signature (64B for sr25519) + commitment = blake2_256(payload_core) + + Notes: + For detailed documentation and examples of MEV Shield protection, see: + + + For creating GenericCall objects to use with this method, see: + + """ + return submit_encrypted_extrinsic( + subtensor=self, + wallet=wallet, + call=call, + sign_with=sign_with, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + blocks_for_revealed_execution=blocks_for_revealed_execution, + ) + + def modify_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + hotkey_ss58: Optional[str] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Example: + + import bittensor as bt + + subtensor = bt.subtensor(network="local") + + my_wallet = bt.Wallet() + + # if liquidity_delta is negative + + my_liquidity_delta = Balance.from_tao(100) * -1 + + subtensor.modify_liquidity( + + wallet=my_wallet, + + netuid=123, + + position_id=2, + + liquidity_delta=my_liquidity_delta + + ) + + # if liquidity_delta is positive + + my_liquidity_delta = Balance.from_tao(120) + + subtensor.modify_liquidity( + + wallet=my_wallet, + + netuid=123, + + position_id=2, + + liquidity_delta=my_liquidity_delta + ) + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + to enable/disable user liquidity. + """ + return modify_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + hotkey_ss58=hotkey_ss58, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def move_stake( + self, + wallet: "Wallet", + origin_netuid: int, + origin_hotkey_ss58: str, + destination_netuid: int, + destination_hotkey_ss58: str, + amount: Optional[Balance] = None, + move_all_stake: bool = False, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Moves stake to a different hotkey and/or subnet. + + Parameters: + wallet: The wallet to move stake from. + origin_netuid: The netuid of the source subnet. + origin_hotkey_ss58: The SS58 address of the source hotkey. + destination_netuid: The netuid of the destination subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + amount: Amount of stake to move. + move_all_stake: If `True`, moves all stake from the source hotkey to the destination hotkey. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Price Protection: + - Rate Limits: + """ + check_balance_amount(amount) + return move_stake_extrinsic( + subtensor=self, + wallet=wallet, + origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + amount=amount, + move_all_stake=move_all_stake, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def poke_deposit( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Adjusts deposits made for proxies and announcements based on current values. + + This method recalculates and updates the locked deposit amounts for both proxy relationships and announcements + for the signing account. It can be used to potentially lower the locked amount if the deposit requirements have + changed (e.g., due to runtime upgrades or changes in the number of proxies/announcements). + + Parameters: + wallet: Bittensor wallet object (the account whose deposits will be adjusted). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are + needed as it operates on the account's current state. + + When to use: + - After runtime upgrade, if deposit constants have changed. + - After removing proxies/announcements, to free up excess locked funds. + - Periodically to optimize locked deposit amounts. + """ + return poke_deposit_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Executes a call on behalf of the real account through a proxy. + + This method allows a proxy account (delegate) to execute a call on behalf of the real account (delegator). The + call is subject to the permissions defined by the proxy type and must respect the delay period if one was set + when the proxy was added. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call must be permitted by the proxy type. For example, a "NonTransfer" proxy cannot execute transfer + calls. The delay period must also have passed since the proxy was added. + """ + return proxy_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def proxy_announced( + self, + wallet: "Wallet", + delegate_ss58: str, + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Executes an announced call on behalf of the real account through a proxy. + + This method executes a call that was previously announced via :meth:`announce_proxy`. The call must match the + call_hash that was announced, and the delay period must have passed since the announcement was made. The real + account has the opportunity to review and reject the announcement before execution. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call_hash of the provided call must match the call_hash that was announced. The announcement must not + have been rejected by the real account, and the delay period must have passed. + """ + return proxy_announced_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def refund_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Refunds contributors from a failed crowdloan campaign that did not reach its cap. + + Refunds are batched, processing up to `RefundContributorsLimit` (default 50) contributors per call. + For campaigns with more contributors, multiple calls are required. Only non-creator contributors are + refunded; the creator's deposit remains until dissolution via `dissolve_crowdloan`. + + Only the crowdloan creator can call this method for a non-finalized crowdloan. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the crowdloan creator). + crowdloan_id: The unique identifier of the crowdloan to refund. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Crowdloans Overview: + - Crowdloan Lifecycle: + - Refund and Dissolve: + """ + return refund_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def reject_proxy_announcement( + self, + wallet: "Wallet", + delegate_ss58: str, + call_hash: str, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Rejects an announcement made by a proxy delegate. + + This method allows the real account to reject an announcement made by a proxy delegate, preventing the announced + call from being executed. Once rejected, the announcement cannot be executed and the announcement deposit is + returned to the delegate. + + Parameters: + wallet: Bittensor wallet object (should be the real account wallet). + delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Once rejected, the announcement cannot be executed. The delegate's announcement deposit is returned. + """ + return reject_announcement_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def register( + self, + wallet: "Wallet", + netuid: int, + max_allowed_attempts: int = 3, + output_in_place: bool = True, + cuda: bool = False, + dev_id: Union[list[int], int] = 0, + tpb: int = 256, + num_processes: Optional[int] = None, + update_interval: Optional[int] = None, + log_verbose: bool = False, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + + Registration is a critical step for a neuron to become an active participant in the network, enabling it to + stake, set weights, and receive incentives. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + max_allowed_attempts: Maximum number of attempts to register the wallet. + output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the + progress is printed on the same lines. + cuda: If `true`, the wallet should be registered using CUDA device(s). + dev_id: The CUDA device id to use, or a list of device ids. + tpb: The number of threads per block (CUDA). + num_processes: The number of processes to use to register. + update_interval: The number of nonces to solve between updates. + log_verbose: If `true`, the registration process will log more information. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function facilitates the entry of new neurons into the network, supporting the decentralized growth and + scalability of the Bittensor ecosystem. + + Notes: + - Rate Limits: + """ + return register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + max_allowed_attempts=max_allowed_attempts, + tpb=tpb, + update_interval=update_interval, + num_processes=num_processes, + cuda=cuda, + dev_id=dev_id, + output_in_place=output_in_place, + log_verbose=log_verbose, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def register_subnet( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers a new subnetwork on the Bittensor network. + + Parameters: + wallet: The wallet to be used for subnet registration. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + return register_subnet_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def remove_proxy_announcement( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Removes an announcement made by a proxy account. + + This method allows the proxy account to remove its own announcement before it is executed or rejected. This + frees up the announcement deposit and prevents the call from being executed. Only the proxy account that made + the announcement can remove it. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Only the proxy account that made the announcement can remove it. The real account can reject it via + :meth:`reject_proxy_announcement`, but cannot remove it directly. + """ + return remove_announcement_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def remove_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + hotkey_ss58: Optional[str] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + hotkey_ss58: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + extrinsic to enable/disable user liquidity. + - To get the `position_id` use `get_liquidity_list` method. + """ + return remove_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + hotkey_ss58=hotkey_ss58, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def remove_proxies( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Removes all proxy relationships for the account in a single transaction. + + This method removes all proxy relationships for the signing account in a single call, which is more efficient + than removing them one by one using :meth:`remove_proxy`. The deposit for all proxies will be returned to the + account. + + Parameters: + wallet: Bittensor wallet object. The account whose proxies will be removed (the delegator). All proxy + relationships where wallet.coldkey.ss58_address is the real account will be removed. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This removes all proxy relationships for the account, regardless of proxy type or delegate. Use + :meth:`remove_proxy` if you need to remove specific proxy relationships selectively. + """ + return remove_proxies_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def remove_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Removes a specific proxy relationship. + + This method removes a single proxy relationship between the real account and a delegate. The parameters must + exactly match those used when the proxy was added via :meth:`add_proxy`. The deposit for this proxy will be returned + to the account. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The delegate_ss58, proxy_type, and delay parameters must exactly match those used when the proxy was added. + Use :meth:`get_proxies_for_real_account` to retrieve the exact parameters for existing proxies. + """ + return remove_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + mechid: int = 0, + max_attempts: int = 5, + version_key: int = version_as_int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = 16, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Parameters: + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + uids: NumPy array of neuron UIDs for which weights are being revealed. + weights: NumPy array of weight values corresponding to each UID. + salt: NumPy array of salt values corresponding to the hash function. + mechid: The subnet mechanism unique identifier. + max_attempts: The number of maximum attempts to reveal weights. + version_key: Version key for compatibility with the network. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency and + accountability within the Bittensor network. + + Notes: + - + - Rate Limits: + """ + attempt = 0 + response = ExtrinsicResponse(False) + + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + + while attempt < max_attempts and response.success is False: + try: + response = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + except Exception as error: + return ExtrinsicResponse.from_exception( + raise_error=raise_error, error=error + ) + attempt += 1 + + if not response.success: + logging.debug("No attempt made. Perhaps it is too soon to reveal weights!") + return response + + def root_register( + self, + wallet: "Wallet", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Register neuron by recycling some TAO. + + Parameters: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def root_set_pending_childkey_cooldown( + self, + wallet: "Wallet", + cooldown: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Sets the pending childkey cooldown. + + Parameters: + wallet: bittensor wallet instance. + cooldown: the number of blocks to setting pending childkey cooldown. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period (Optional[int]): The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: This operation can only be successfully performed if your wallet has root privileges. + """ + return root_set_pending_childkey_cooldown_extrinsic( + subtensor=self, + wallet=wallet, + cooldown=cooldown, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_auto_stake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Sets the coldkey to automatically stake to the hotkey within specific subnet mechanism. + + Parameters: + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + hotkey_ss58: The SS58 address of the validator's hotkey to which the miner automatically stakes all rewards + received from the specified subnet immediately upon receipt. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Use the `get_auto_stakes` method to get the hotkey address of the validator where auto stake is set. + """ + return set_auto_stake_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_children( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + children: list[tuple[float, str]], + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Allows a coldkey to set children-keys. + + Parameters: + wallet: bittensor wallet instance. + hotkey_ss58: The `SS58` address of the neuron's hotkey. + netuid: The netuid value. + children: A list of children with their proportions. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Rate Limits: + """ + return set_children_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + children=children, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + period: Optional[int] = DEFAULT_PERIOD, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Parameters: + wallet: bittensor wallet instance. + hotkey_ss58: The `SS58` address of the neuron's hotkey. + take: Percentage reward for the delegate. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exist. + NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated + with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + + Notes: + - Rate Limits: + """ + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + message = f"The take for {hotkey_ss58} is already set to {take}." + logging.debug(f"[green]{message}[/green].") + return ExtrinsicResponse(True, message) + + logging.debug(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + response = set_take_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + take=take_u16, + action="increase_take" if current_take_u16 < take_u16 else "decrease_take", + period=period, + raise_error=raise_error, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if response.success: + return response + + logging.error(f"[red]{response.message}[/red]") + return response + + def set_root_claim_type( + self, + wallet: "Wallet", + new_root_claim_type: "Literal['Swap', 'Keep'] | RootClaimType | dict", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ): + """Submit an extrinsic to set the root claim type for the wallet's coldkey. + + The root claim type determines how future Alpha dividends from subnets are handled when they are claimed for + the wallet's coldkey: + + - `Swap`: Alpha dividends are swapped to TAO at claim time and restaked on the Root Subnet (default). + - `Keep`: Alpha dividends remain as Alpha on the originating subnets. + + Parameters: + + wallet: Bittensor `Wallet` instance. + new_root_claim_type: The new root claim type to set. Can be: + - String: "Swap" or "Keep" + - RootClaimType: RootClaimType.Swap, RootClaimType.Keep + - Dict: {"KeepSubnets": {"subnets": [1, 2, 3]}} + - Callable: RootClaimType.KeepSubnets([1, 2, 3]) + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: Number of blocks for which the transaction remains valid after submission. If the extrinsic is + not included in a block within this window, it will expire and be rejected. + raise_error: Whether to raise a Python exception instead of returning a failed `ExtrinsicResponse`. + wait_for_inclusion: Whether to wait until the extrinsic is included in a block before returning. + wait_for_finalization: Whether to wait for finalization of the extrinsic in a block before returning. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` describing the result of the extrinsic execution. + + Notes: + - This setting applies to both automatic and manual root claims going forward; it does not retroactively + change how already-claimed dividends were processed. + - Only the treatment of Alpha dividends is affected; the underlying TAO stake on the Root Subnet is + unchanged. + - See: + - See also: + """ + return set_root_claim_type_extrinsic( + subtensor=self, + wallet=wallet, + new_root_claim_type=new_root_claim_type, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_subnet_identity( + self, + wallet: "Wallet", + netuid: int, + subnet_identity: SubnetIdentity, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Sets the identity of a subnet for a specific wallet and network. + + Parameters: + wallet: The wallet instance that will authorize the transaction. + netuid: The unique ID of the network on which the operation takes place. + subnet_identity: The identity data of the subnet including attributes like name, GitHub repository, contact, + URL, discord, description, and any additional metadata. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return set_subnet_identity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + subnet_name=subnet_identity.subnet_name, + github_repo=subnet_identity.github_repo, + subnet_contact=subnet_identity.subnet_contact, + subnet_url=subnet_identity.subnet_url, + logo_url=subnet_identity.logo_url, + discord=subnet_identity.discord, + description=subnet_identity.description, + additional=subnet_identity.additional, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: UIDs, + weights: Weights, + mechid: int = 0, + block_time: float = 12.0, + commit_reveal_version: int = 4, + max_attempts: int = 5, + version_key: int = version_as_int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Sets the interneuronal weights for the specified neuron. This process involves specifying the influence or trust + a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized + learning architecture. + + Parameters: + wallet: The wallet associated with the subnet validator setting the weights. + netuid: The unique identifier of the subnet. + uids: The list of subnet miner neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID, representing the validator's evaluation of each + miner's performance. + mechid: The subnet mechanism unique identifier. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the chain commit-reveal protocol to use. + max_attempts: The number of maximum attempts to set weights. + version_key: Version key for compatibility with the network. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function is crucial in shaping the network's collective intelligence, where each neuron's learning and + contribution are influenced by the weights it sets towards others. + + Notes: + - + - Rate Limits: + """ + attempt = 0 + response = ExtrinsicResponse(False) + if attempt_check := validate_max_attempts(max_attempts, response): + return attempt_check + + def _blocks_weight_limit() -> bool: + bslu = cast(int, self.blocks_since_last_update(netuid, cast(int, uid))) + wrl = cast(int, self.weights_rate_limit(netuid)) + return bslu > wrl + + if ( + uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + ) is None: + return ExtrinsicResponse( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}.", + ) + + if self.commit_reveal_enabled(netuid=netuid): + # go with `commit_reveal_weights_extrinsic` extrinsic + + while ( + attempt < max_attempts + and response.success is False + and _blocks_weight_limit() + ): + logging.debug( + f"Committing weights {weights} for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{attempt + 1}[blue] of [green]{max_attempts}[/green]." + ) + try: + response = commit_timelocked_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + block_time=block_time, + commit_reveal_version=commit_reveal_version, + version_key=version_key, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + except Exception as error: + return ExtrinsicResponse.from_exception( + raise_error=raise_error, error=error + ) + attempt += 1 + else: + # go with `set_mechanism_weights_extrinsic` + + while ( + attempt < max_attempts + and response.success is False + and _blocks_weight_limit() + ): + try: + logging.debug( + f"Setting weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{attempt + 1}[/blue] of [green]{max_attempts}[/green]." + ) + response = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + version_key=version_key, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + except Exception as error: + return ExtrinsicResponse.from_exception( + raise_error=raise_error, error=error + ) + attempt += 1 + + if not response.success: + logging.debug( + "No one successful attempt made. Perhaps it is too soon to set weights!" + ) + return response + + def serve_axon( + self, + netuid: int, + axon: "Axon", + certificate: Optional[Certificate] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Registers an `Axon` serving endpoint on the Bittensor network for a specific neuron. + + This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data + processing tasks. + + Parameters: + netuid: The unique identifier of the subnetwork. + axon: The Axon instance to be registered for serving. + certificate: Certificate to use for TLS. If `None`, no TLS will be used. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, it will expire + and be rejected. You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, + contributing to the collective intelligence of Bittensor. + + Notes: + - Rate Limits: + """ + return serve_axon_extrinsic( + subtensor=self, + netuid=netuid, + axon=axon, + certificate=certificate, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_commitment( + self, + wallet: "Wallet", + netuid: int, + data: str, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + This method allows neurons to publish arbitrary data to the blockchain, which can be used for various purposes + such as sharing model updates, configuration data, or other network-relevant information. + + + Parameters: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Example: + + # Commit some data to subnet 1 + + response = await subtensor.commit(wallet=my_wallet, netuid=1, data="Hello Bittensor!") + + # Commit with custom period + + response = await subtensor.commit(wallet=my_wallet, netuid=1, data="Model update v2.0", period=100) + + Note: See + """ + return publish_metadata_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"Raw{len(data)}", + data=data.encode(), + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def set_reveal_commitment( + self, + wallet, + netuid: int, + data: str, + blocks_until_reveal: int = 360, + block_time: Union[int, float] = 12, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Parameters: + wallet: The wallet associated with the neuron committing the data. + netuid: The unique identifier of the subnetwork. + data: The data to be committed to the network. + blocks_until_reveal: The number of blocks from now after which the data will be revealed. Then number of + blocks in one epoch. + block_time: The number of seconds between each block. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. + Successful extrinsic's the "data" field contains {"encrypted": encrypted, "reveal_round": reveal_round}. + """ + + encrypted, reveal_round = get_encrypted_commitment( + data, blocks_until_reveal, block_time + ) + + data_ = {"encrypted": encrypted, "reveal_round": reveal_round} + response = publish_metadata_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type="TimelockEncrypted", + data=data_, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + response.data = data_ + return response + + def start_call( + self, + wallet: "Wallet", + netuid: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start + a new subnet's emission mechanism). + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return start_call_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def swap_stake( + self, + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. + Like subnet hopping - same owner, same hotkey, just changing which subnet the stake is in. + + Parameters: + wallet: The wallet to swap stake from. + hotkey_ss58: The SS58 address of the hotkey whose stake is being swapped. + origin_netuid: The netuid from which stake is removed. + destination_netuid: The netuid to which stake is added. + amount: The amount to swap. + safe_swapping: If `True`, enables price safety checks to protect against fluctuating prices. The swap + will only execute if the price ratio between subnets doesn't exceed the rate tolerance. + allow_partial_stake: If `True` and safe_staking is enabled, allows partial stake swaps when the full amount + would exceed the price tolerance. If false, the entire swap fails if it would exceed the tolerance. + rate_tolerance: The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when + safe_staking is True. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price. + When `safe_swapping` is enabled, the swap will only execute if: + - With `allow_partial_stake=False`: The entire swap amount can be executed without the price ratio + increasing more than `rate_tolerance`. + - With `allow_partial_stake=True`: A partial amount will be swapped up to the point where the price ratio + would increase by `rate_tolerance`. + - Price Protection: + - Rate Limits: + """ + check_balance_amount(amount) + return swap_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + safe_swapping=safe_swapping, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def toggle_user_liquidity( + self, + wallet: "Wallet", + netuid: int, + enable: bool, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Allow to toggle user liquidity for specified subnet. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: The call can be executed successfully by the subnet owner only. + """ + return toggle_user_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + enable=enable, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def transfer( + self, + wallet: "Wallet", + destination_ss58: str, + amount: Optional[Balance], + transfer_all: bool = False, + keep_alive: bool = True, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Transfer token of amount to destination. + + Parameters: + wallet: Source wallet for the transfer. + destination_ss58: Destination address for the transfer. + amount: Number of tokens to transfer. `None` is transferring all. + transfer_all: Flag to transfer all tokens. + keep_alive: Flag to keep the connection alive. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + check_balance_amount(amount) + return transfer_extrinsic( + subtensor=self, + wallet=wallet, + destination_ss58=destination_ss58, + amount=amount, + transfer_all=transfer_all, + keep_alive=keep_alive, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def transfer_stake( + self, + wallet: "Wallet", + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Transfers stake from one subnet to another while changing the coldkey owner. + + Parameters: + wallet: The wallet to transfer stake from. + destination_coldkey_ss58: The destination coldkey SS58 address. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to transfer. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Notes: + - Price Protection: + - Rate Limits: + """ + check_balance_amount(amount) + return transfer_stake_extrinsic( + subtensor=self, + wallet=wallet, + destination_coldkey_ss58=destination_coldkey_ss58, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def unstake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + safe_unstaking: bool = False, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting + individual neuron stakes within the Bittensor network. + + Parameters: + wallet: The wallet associated with the neuron from which the stake is being removed. + netuid: The unique identifier of the subnet. + hotkey_ss58: The `SS58` address of the hotkey account to unstake from. + amount: The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. + allow_partial_stake: If `True` and safe_staking is enabled, allows partial unstaking when + the full amount would exceed the price tolerance. If false, the entire unstake fails if it would + exceed the tolerance. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, + 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. + safe_unstaking: If `True`, enables price safety checks to protect against fluctuating prices. The unstake + will only execute if the price change doesn't exceed the rate tolerance. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function supports flexible stake management, allowing neurons to adjust their network participation and + potential reward accruals. When safe_staking is enabled, it provides protection against price fluctuations + during the time unstake is executed and the time it is actually processed by the chain. + + Notes: + - Price Protection: + - Rate Limits: + """ + check_balance_amount(amount) + return unstake_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + safe_unstaking=safe_unstaking, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def unstake_all( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + rate_tolerance: Optional[float] = 0.005, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Parameters: + wallet: The wallet of the stake owner. + netuid: The unique identifier of the subnet. + hotkey_ss58: The SS58 address of the hotkey to unstake from. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Example: + + # If you would like to unstake all stakes in all subnets safely: + + import bittensor as bt + + subtensor = bt.Subtensor() + + wallet = bt.Wallet("my_wallet") + + netuid = 14 + + hotkey = "5%SOME_HOTKEY%" + + wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + + result = subtensor.unstake_all( + + wallet=wallet, + + hotkey_ss58=stake.hotkey_ss58, + + netuid=stake.netuid, + + ) + + print(result) + + # If you would like to unstake all stakes in all subnets unsafely, use rate_tolerance=None: + + import bittensor as bt + + subtensor = bt.Subtensor() + + wallet = bt.Wallet("my_wallet") + + netuid = 14 + + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + + result = subtensor.unstake_all( + + wallet=wallet, + + hotkey_ss58=stake.hotkey_ss58, + + netuid=stake.netuid, + + rate_tolerance=None, + + ) + + print(result) + + Notes: + - Price Protection: + - Rate Limits: + """ + return unstake_all_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + rate_tolerance=rate_tolerance, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def unstake_multiple( + self, + wallet: "Wallet", + netuids: UIDs, + hotkey_ss58s: list[str], + amounts: Optional[list[Balance]] = None, + unstake_all: bool = False, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts + efficiently. This function is useful for managing the distribution of stakes across multiple neurons. + + Parameters: + wallet: The wallet linked to the coldkey from which the stakes are being withdrawn. + netuids: Subnets unique IDs. + hotkey_ss58s: A list of hotkey `SS58` addresses to unstake from. + amounts: The amounts of TAO to unstake from each hotkey. If not provided, unstakes all. + unstake_all: If `True`, unstakes all tokens. Amounts are ignored. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake + management aspect of the Bittensor network. + + Notes: + - Price Protection: + - Rate Limits: + """ + return unstake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + unstake_all=unstake_all, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def update_cap_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + new_cap: "Balance", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Updates the fundraising cap of an active (non-finalized) crowdloan. + + Allows the creator to adjust the maximum total contribution amount before finalization. The new cap + must be at least equal to the amount already raised. This is useful for adjusting campaign goals based + on contributor feedback or changing subnet costs. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). + crowdloan_id: The unique identifier of the crowdloan to update. + new_cap: The new fundraising cap (TAO). Must be `>= raised`. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Only the creator can update the cap. + - The crowdloan must not be finalized. + - The new cap must be `>=` the total funds already raised. + + - Crowdloans Overview: + - Update Parameters: + """ + return update_cap_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + new_cap=new_cap, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def update_end_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + new_end: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Updates the end block of an active (non-finalized) crowdloan. + + Allows the creator to extend (or shorten) the contribution period before finalization. The new end block + must be in the future and respect the minimum and maximum duration bounds defined in the runtime constants. + This is useful for extending campaigns that need more time to reach their cap or shortening campaigns with + sufficient contributions. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). + crowdloan_id: The unique identifier of the crowdloan to update. + new_end: The new block number at which the crowdloan will end. Must be between `MinimumBlockDuration` + (7 days = 50,400 blocks) and `MaximumBlockDuration` (60 days = 432,000 blocks) from the current block. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Only the creator can update the end block. + - The crowdloan must not be finalized. + - The new end block must respect duration bounds (`MinimumBlockDuration` to `MaximumBlockDuration`). + + - Crowdloans Overview: + - Update Parameters: + """ + return update_end_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + new_end=new_end, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def update_min_contribution_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + new_min_contribution: "Balance", + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Updates the minimum contribution amount of an active (non-finalized) crowdloan. + + Allows the creator to adjust the minimum per-contribution amount before finalization. The new value must + meet or exceed the `AbsoluteMinimumContribution` constant. This is useful for adjusting contribution + requirements based on the number of expected contributors or campaign strategy. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (must be the creator's coldkey). + crowdloan_id: The unique identifier of the crowdloan to update. + new_min_contribution: The new minimum contribution amount (TAO). Must be `>= AbsoluteMinimumContribution`. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission. + raise_error: If `True`, raises an exception rather than returning failure in the response. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + - Only the creator can update the minimum contribution. + - The crowdloan must not be finalized. + - The new minimum must be `>= AbsoluteMinimumContribution` (check via `get_crowdloan_constants`). + + - Crowdloans Overview: + - Update Parameters: + """ + return update_min_contribution_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + new_min_contribution=new_min_contribution, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + def withdraw_crowdloan( + self, + wallet: "Wallet", + crowdloan_id: int, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """Withdraws a contribution from an active (not yet finalized or dissolved) crowdloan. + + Contributors can withdraw their contributions at any time before finalization. For regular contributors, + the full contribution amount is returned. For the creator, only amounts exceeding the initial deposit can + be withdrawn; the deposit itself remains locked until dissolution. + + Parameters: + wallet: Bittensor wallet instance used to sign the transaction (coldkey must match a contributor). + crowdloan_id: The unique identifier of the crowdloan to withdraw from. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after submission, after which + it will be rejected. + raise_error: If `True`, raises an exception rather than returning False in the response, in case the + transaction fails. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + `ExtrinsicResponse` indicating success or failure, with error details if applicable. + + Notes: + + - Crowdloans Overview: + - Crowdloan Lifecycle: + - Withdraw: + """ + return withdraw_crowdloan_extrinsic( + subtensor=self, + wallet=wallet, + crowdloan_id=crowdloan_id, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) From e252f4dfde2cedeef523f92fb9e9fa8e68841b01 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 10:40:54 -0800 Subject: [PATCH 16/40] Refactor result handling in subtensor.py Simplify return statement to handle result value extraction. --- bittensor/core/subtensor.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 8665e79327..7c9787f6a1 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -570,15 +570,8 @@ def get_hyperparameter( params=[netuid], block_hash=block_hash, ) - - if result is None: - return None - - if hasattr(result, "value"): - return result.value - - return result - + return getattr(result, "value", result) + @property def block(self) -> int: """Provides an asynchronous getter to retrieve the current block number. From c62cb04fbff9de6f58bd49fe838c1a15cdd31462 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 10:42:11 -0800 Subject: [PATCH 17/40] remove unused imports Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit_tests/utils/test_retry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit_tests/utils/test_retry.py b/tests/unit_tests/utils/test_retry.py index 861ab45cca..f9aa9977ee 100644 --- a/tests/unit_tests/utils/test_retry.py +++ b/tests/unit_tests/utils/test_retry.py @@ -1,5 +1,4 @@ import pytest -import time import asyncio from unittest.mock import Mock, patch, AsyncMock from bittensor.utils.retry import retry_call, retry_async From 3f5d254e6d98abcac5e694f0fa489ee2b2db3e63 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 10:42:47 -0800 Subject: [PATCH 18/40] remove unused imports Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit_tests/utils/test_retry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit_tests/utils/test_retry.py b/tests/unit_tests/utils/test_retry.py index f9aa9977ee..00320bbfd3 100644 --- a/tests/unit_tests/utils/test_retry.py +++ b/tests/unit_tests/utils/test_retry.py @@ -1,5 +1,4 @@ import pytest -import asyncio from unittest.mock import Mock, patch, AsyncMock from bittensor.utils.retry import retry_call, retry_async From a3f359e8a7936d6f98015fc7fc622df0079b656a Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 11:12:50 -0800 Subject: [PATCH 19/40] Refactor runtime_call to a single return statement --- bittensor/core/async_subtensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 3207e908e3..c54eac656a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -965,10 +965,12 @@ async def query_runtime_api( block_hash = await self.determine_block_hash(block, block_hash, reuse_block) if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - result = await self.substrate.runtime_call( - runtime_api, method, params, block_hash - ) - return result.value + return (await self.substrate.runtime_call( + api=runtime_api, + method=method, + params=params, + block_hash=block_hash, + )).value async def query_subtensor( self, From 201ec87d484f4baa216c4ec1f31d85aaafff5ead Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 11:33:25 -0800 Subject: [PATCH 20/40] Validate environment variables for retry settings --- bittensor/utils/retry.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 53e3ff6d6b..2a3a05b13b 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -29,7 +29,15 @@ def _retry_max_attempts() -> int: if raw is None or raw == "": return default try: - return int(raw) + value = int(raw) + if value <= 0: + logger.warning( + "Invalid value for BT_RETRY_MAX_ATTEMPTS=%r (must be positive); falling back to default %d", + raw, + default, + ) + return default + return value except (TypeError, ValueError): logger.warning( "Invalid value for BT_RETRY_MAX_ATTEMPTS=%r; falling back to default %d", @@ -46,7 +54,15 @@ def _retry_base_delay() -> float: if raw is None or raw == "": return default try: - return float(raw) + value = float(raw) + if value < 0: + logger.warning( + "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %. 2f", + raw, + default, + ) + return default + return value except (TypeError, ValueError): logger.warning( "Invalid value for BT_RETRY_BASE_DELAY=%r; falling back to default %.2f", @@ -59,11 +75,19 @@ def _retry_base_delay() -> float: def _retry_max_delay() -> float: """Get the maximum delay (in seconds) for retries from the environment, with validation.""" default = 60.0 - raw = os.environ.get("BT_RETRY_MAX_DELAY") + raw = os. environ.get("BT_RETRY_MAX_DELAY") if raw is None or raw == "": return default try: - return float(raw) + value = float(raw) + if value < 0: + logger.warning( + "Invalid value for BT_RETRY_MAX_DELAY=%r (must be non-negative); falling back to default %.2f", + raw, + default, + ) + return default + return value except (TypeError, ValueError): logger.warning( "Invalid value for BT_RETRY_MAX_DELAY=%r; falling back to default %.2f", From 6f56a489e1d44ceaf92e24062d6453b823c50e22 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 11:54:09 -0800 Subject: [PATCH 21/40] Refactor retry logic and improve error handling --- bittensor/utils/retry.py | 88 +++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 2a3a05b13b..32c43feb04 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -1,4 +1,5 @@ import asyncio +import inspect import os import time import random @@ -26,9 +27,9 @@ def _retry_max_attempts() -> int: """Get the maximum number of retry attempts from the environment, with validation.""" default = 3 raw = os.environ.get("BT_RETRY_MAX_ATTEMPTS") - if raw is None or raw == "": + if raw is None or raw == "": return default - try: + try: value = int(raw) if value <= 0: logger.warning( @@ -57,7 +58,7 @@ def _retry_base_delay() -> float: value = float(raw) if value < 0: logger.warning( - "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %. 2f", + "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %.2f", raw, default, ) @@ -75,7 +76,7 @@ def _retry_base_delay() -> float: def _retry_max_delay() -> float: """Get the maximum delay (in seconds) for retries from the environment, with validation.""" default = 60.0 - raw = os. environ.get("BT_RETRY_MAX_DELAY") + raw = os.environ.get("BT_RETRY_MAX_DELAY") if raw is None or raw == "": return default try: @@ -101,7 +102,28 @@ def _retry_max_delay() -> float: def _retry_backoff_factor() -> float: - return float(os.environ.get("BT_RETRY_BACKOFF_FACTOR", _RETRY_BACKOFF_FACTOR)) + """Get the backoff factor for exponential backoff from the environment, with validation.""" + default = _RETRY_BACKOFF_FACTOR + raw = os.environ.get("BT_RETRY_BACKOFF_FACTOR") + if raw is None or raw == "": + return default + try: + value = float(raw) + if value <= 0: + logger.warning( + "Invalid value for BT_RETRY_BACKOFF_FACTOR=%r (must be positive); falling back to default %.2f", + raw, + default, + ) + return default + return value + except (TypeError, ValueError): + logger.warning( + "Invalid value for BT_RETRY_BACKOFF_FACTOR=%r; falling back to default %.2f", + raw, + default, + ) + return default def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> float: @@ -112,14 +134,14 @@ def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> floa def retry_call( - func: Callable, + func: Callable, *args, retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = ( OSError, TimeoutError, ), max_attempts: Optional[int] = None, - base_delay: Optional[float] = None, + base_delay: Optional[float] = None, max_delay: Optional[float] = None, **kwargs, ) -> Any: @@ -139,7 +161,7 @@ def retry_call( *args Positional arguments forwarded to ``func``. retry_exceptions : Exception type or tuple of Exception types, optional - Exception type(s) that should trigger a retry. Any exception that is + Exception type(s) that should trigger a retry. Any exception that is not an instance of these types is raised immediately without further retry attempts. Defaults to ``(OSError, TimeoutError)``. max_attempts : int, optional @@ -164,6 +186,8 @@ def retry_call( Raises ------ + TypeError + If ``func`` is an async function. Use ``async_retry_call`` instead. Exception Any exception raised by ``func`` when retries are disabled (``BT_RETRY_ENABLED`` is falsey), or when the exception type is not @@ -193,6 +217,13 @@ def retry_call( falsey value such as ``"false"`` or ``"0"``. In that case ``retry_call`` simply calls ``func(*args, **kwargs)`` once and propagates any exception. """ + # Validate that func is not async + if inspect.iscoroutinefunction(func): + raise TypeError( + f"retry_call() cannot be used with async functions. " + f"Use async_retry_call() instead for {func.__name__}." + ) + if not _retry_enabled(): return func(*args, **kwargs) @@ -201,18 +232,15 @@ def retry_call( _base_delay = base_delay if base_delay is not None else _retry_base_delay() _max_delay = max_delay if max_delay is not None else _retry_max_delay() - last_exception = None - for attempt in range(1, _max_attempts + 1): try: return func(*args, **kwargs) except retry_exceptions as e: - last_exception = e - if attempt == _max_attempts: + if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) - raise e + raise backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) logger.debug( @@ -220,12 +248,11 @@ def retry_call( ) time.sleep(backoff) - if last_exception: - raise last_exception - return None # Should not be reached + # This should never be reached due to the logic above + assert False, "Unreachable code" -async def retry_async( +async def async_retry_call( func: Callable, *args, retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = ( @@ -274,7 +301,7 @@ async def retry_async( the value is taken from the ``BT_RETRY_MAX_ATTEMPTS`` environment variable (default 3 when unset). base_delay : float, optional - Base delay in seconds before the first retry. If ``None``, the value is + Base delay in seconds before the first retry. If ``None``, the value is taken from the ``BT_RETRY_BASE_DELAY`` environment variable (default 1.0 when unset). max_delay : float, optional @@ -291,6 +318,8 @@ async def retry_async( Raises ------ + TypeError + If ``func`` is not an async function. Use ``retry_call`` instead. Exception Any exception raised by ``func`` when retries are disabled. retry_exceptions @@ -307,14 +336,14 @@ async def retry_async( async def fetch(): ... - result = await retry_async(fetch) + result = await async_retry_call(fetch) - Overriding retry configuration and the set of retryable exceptions:: + Overriding retry configuration and the set of retryable exceptions:: async def fetch_with_timeout(): ... - result = await retry_async( + result = await async_retry_call( fetch_with_timeout, retry_exceptions=(OSError, TimeoutError, ConnectionError), max_attempts=5, @@ -322,6 +351,13 @@ async def fetch_with_timeout(): max_delay=10.0, ) """ + # Validate that func is async + if not inspect.iscoroutinefunction(func): + raise TypeError( + f"async_retry_call() requires an async function. " + f"Use retry_call() instead for {func.__name__}." + ) + if not _retry_enabled(): return await func(*args, **kwargs) @@ -330,18 +366,15 @@ async def fetch_with_timeout(): _base_delay = base_delay if base_delay is not None else _retry_base_delay() _max_delay = max_delay if max_delay is not None else _retry_max_delay() - last_exception = None - for attempt in range(1, _max_attempts + 1): try: return await func(*args, **kwargs) except retry_exceptions as e: - last_exception = e if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) - raise e + raise backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) logger.debug( @@ -349,6 +382,5 @@ async def fetch_with_timeout(): ) await asyncio.sleep(backoff) - if last_exception: - raise last_exception - return None # Should not be reached + # This should never be reached due to the logic above + assert False, "Unreachable code" From 5d3e99a49fe2b3d36848622110f26009781067a3 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:06:18 -0800 Subject: [PATCH 22/40] Remove unnecessary blank lines in dendrite.py --- bittensor/core/dendrite.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index f2cb36f717..c1b1db53ac 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -8,14 +8,12 @@ import aiohttp from bittensor_wallet import Keypair, Wallet - from bittensor.core.axon import Axon from bittensor.core.chain_data import AxonInfo from bittensor.core.settings import version_as_int from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.utils import networking - from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch From 717c6a000b352a8c0e431bb78337b6e31032bed7 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:10:12 -0800 Subject: [PATCH 23/40] Clarify HTTP POST request with a comment Added a comment to clarify the HTTP POST request section. --- bittensor/core/dendrite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index c1b1db53ac..d8a7c002af 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -648,6 +648,7 @@ async def call_stream( # Log outgoing request self._log_outgoing_request(synapse) + # Make the HTTP POST request async with (await self.session).post( url, headers=synapse.to_headers(), From 50dbdd2c8bda738e9ad5d049b77f96cc7fb40002 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:11:32 -0800 Subject: [PATCH 24/40] Remove unnecessary blank lines in subtensor.py --- bittensor/core/subtensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7c9787f6a1..124fbf9487 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -164,13 +164,11 @@ tick_to_price, ) - if TYPE_CHECKING: from async_substrate_interface.sync_substrate import QueryMapResult from bittensor_wallet import Keypair, Wallet from scalecodec.types import GenericCall - class Subtensor(SubtensorMixin): """Synchronous interface for interacting with the Bittensor blockchain. From d4b8a81e1ad3c57a394c4fe9a25d2cb18544348a Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:16:11 -0800 Subject: [PATCH 25/40] Fix formatting and improve comments in retry.py --- bittensor/utils/retry.py | 254 +++++++++++++-------------------------- 1 file changed, 84 insertions(+), 170 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 32c43feb04..ca6bb88a03 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -1,3 +1,24 @@ +"""Retry utilities for handling transient failures with exponential backoff. + +This module provides optional retry wrappers for both synchronous and asynchronous +functions. Retry behavior is controlled via environment variables and is disabled +by default. + +Environment Variables: + BT_RETRY_ENABLED: Enable retry behavior ("true", "1", "yes", "on") + BT_RETRY_MAX_ATTEMPTS: Maximum retry attempts (default: 3) + BT_RETRY_BASE_DELAY: Base delay in seconds (default: 1.0) + BT_RETRY_MAX_DELAY: Maximum delay in seconds (default: 60.0) + BT_RETRY_BACKOFF_FACTOR: Exponential backoff multiplier (default: 2.0) + +Note: + This utility is not used internally by the SDK. It is provided as an + optional helper for users who wish to implement consistent retry behavior. + +For more information on retry strategies, see: + https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ +""" + import asyncio import inspect import os @@ -6,11 +27,7 @@ import logging from typing import Type, Tuple, Optional, Callable, Any, Union -logger = logging.getLogger("bittensor.utils.retry") - -# Note: This utility is not used internally by the SDK. -# It is provided as an optional helper for users who wish -# to implement consistent retry behavior themselves. +logger = logging.getLogger("bittensor. utils.retry") # Helpers for runtime environment variable access @@ -27,9 +44,9 @@ def _retry_max_attempts() -> int: """Get the maximum number of retry attempts from the environment, with validation.""" default = 3 raw = os.environ.get("BT_RETRY_MAX_ATTEMPTS") - if raw is None or raw == "": + if raw is None or raw == "": return default - try: + try: value = int(raw) if value <= 0: logger.warning( @@ -58,7 +75,7 @@ def _retry_base_delay() -> float: value = float(raw) if value < 0: logger.warning( - "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %.2f", + "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %. 2f", raw, default, ) @@ -145,77 +162,33 @@ def retry_call( max_delay: Optional[float] = None, **kwargs, ) -> Any: - """ - Synchronous retry wrapper around ``func`` with optional exponential backoff. - - When the environment variable ``BT_RETRY_ENABLED`` is set to a truthy value - (e.g. ``"true"``, ``"1"``, ``"yes"``, ``"on"``), the call to ``func`` will be - retried on failure up to ``max_attempts`` times using exponential backoff with - jitter. When ``BT_RETRY_ENABLED`` is false or unset, ``func`` is executed - exactly once and any exception it raises is propagated immediately. - - Parameters - ---------- - func : Callable - The callable to be executed and potentially retried. - *args - Positional arguments forwarded to ``func``. - retry_exceptions : Exception type or tuple of Exception types, optional - Exception type(s) that should trigger a retry. Any exception that is - not an instance of these types is raised immediately without further - retry attempts. Defaults to ``(OSError, TimeoutError)``. - max_attempts : int, optional - Maximum number of attempts (initial attempt + retries) that will be - made before giving up. If ``None``, the value is taken from the - ``BT_RETRY_MAX_ATTEMPTS`` environment variable (default ``3``). - base_delay : float, optional - Base delay, in seconds, used for exponential backoff before applying - jitter. If ``None``, the value is taken from the - ``BT_RETRY_BASE_DELAY`` environment variable (default ``1.0``). - max_delay : float, optional - Maximum delay, in seconds, between attempts. The computed backoff - value will not exceed this. If ``None``, the value is taken from the - ``BT_RETRY_MAX_DELAY`` environment variable (default ``60.0``). - **kwargs - Keyword arguments forwarded to ``func``. - - Returns - ------- - Any - The return value of ``func`` from the first successful attempt. - - Raises - ------ - TypeError - If ``func`` is an async function. Use ``async_retry_call`` instead. - Exception - Any exception raised by ``func`` when retries are disabled - (``BT_RETRY_ENABLED`` is falsey), or when the exception type is not - included in ``retry_exceptions``. When retries are enabled and all - attempts fail with a ``retry_exceptions`` type, the last such - exception is re-raised after the final attempt. - - Examples - -------- - Basic usage with defaults:: - - result = retry_call(do_network_request, url, timeout=5) - - Custom retry configuration:: - - result = retry_call( - do_network_request, - url, - timeout=5, - max_attempts=5, - base_delay=0.5, - max_delay=10.0, - retry_exceptions=(OSError, TimeoutError, ConnectionError), - ) - - To disable retries entirely, unset ``BT_RETRY_ENABLED`` or set it to a - falsey value such as ``"false"`` or ``"0"``. In that case ``retry_call`` - simply calls ``func(*args, **kwargs)`` once and propagates any exception. + """Synchronous retry wrapper with optional exponential backoff. + + Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. + When disabled, the function executes exactly once. + + Args: + func: The callable to be executed and potentially retried. + *args: Positional arguments forwarded to func. + retry_exceptions: Exception type(s) that trigger a retry. Any exception + not matching these types is raised immediately. Defaults to + (OSError, TimeoutError). + max_attempts: Maximum number of attempts. If None, uses + BT_RETRY_MAX_ATTEMPTS environment variable (default: 3). + base_delay: Base delay in seconds for exponential backoff. If None, + uses BT_RETRY_BASE_DELAY environment variable (default: 1.0). + max_delay: Maximum delay in seconds between attempts. If None, uses + BT_RETRY_MAX_DELAY environment variable (default: 60.0). + **kwargs: Keyword arguments forwarded to func. + + Returns: + The return value from the first successful func execution. + + Raises: + TypeError: If func is an async function. Use async_retry_call instead. + Exception: Any exception raised by func when retries are disabled, or + when the exception type doesn't match retry_exceptions, or after + all retry attempts are exhausted. """ # Validate that func is not async if inspect.iscoroutinefunction(func): @@ -236,7 +209,7 @@ def retry_call( try: return func(*args, **kwargs) except retry_exceptions as e: - if attempt == _max_attempts: + if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) @@ -244,7 +217,7 @@ def retry_call( backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) logger.debug( - f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." + f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." ) time.sleep(backoff) @@ -264,97 +237,38 @@ async def async_retry_call( max_delay: Optional[float] = None, **kwargs, ) -> Any: - """ - Asynchronously call a function with optional retry logic and exponential backoff. - - When retries are **disabled** (``BT_RETRY_ENABLED`` is unset or set to a falsy - value such as ``"false"`` or ``"0"``), this helper makes a single call: - - * ``await func(*args, **kwargs)`` is executed exactly once. - * Any exception raised by ``func`` is propagated immediately. - - When retries are **enabled** (``BT_RETRY_ENABLED`` is set to a truthy value - such as ``"true"`` or ``"1"``), the call will be retried on a configurable - set of exceptions using exponential backoff with jitter: - - * The number of attempts defaults to ``BT_RETRY_MAX_ATTEMPTS`` (int, default 3) - and can be overridden via ``max_attempts``. - * The initial delay between attempts defaults to ``BT_RETRY_BASE_DELAY`` - (float seconds, default 1.0) and can be overridden via ``base_delay``. - * The delay is multiplied by an internal backoff factor on each attempt and - capped by ``BT_RETRY_MAX_DELAY`` (float seconds, default 60.0), which can - be overridden via ``max_delay``. - - Parameters - ---------- - func : Callable - Asynchronous callable to execute. This must return an awaitable and is - called as ``await func(*args, **kwargs)``. - *args : - Positional arguments forwarded to ``func`` on each attempt. - retry_exceptions : Exception type or tuple of Exception types, optional - Exception type(s) that trigger a retry when raised by ``func``. - Any exception not matching ``retry_exceptions`` is propagated immediately - without further retries. Defaults to ``(OSError, TimeoutError)``. - max_attempts : int, optional - Maximum number of attempts (including the first attempt). If ``None``, - the value is taken from the ``BT_RETRY_MAX_ATTEMPTS`` environment - variable (default 3 when unset). - base_delay : float, optional - Base delay in seconds before the first retry. If ``None``, the value is - taken from the ``BT_RETRY_BASE_DELAY`` environment variable (default - 1.0 when unset). - max_delay : float, optional - Maximum delay in seconds between retries. If ``None``, the value is - taken from the ``BT_RETRY_MAX_DELAY`` environment variable (default - 60.0 when unset). - **kwargs : - Keyword arguments forwarded to ``func`` on each attempt. - - Returns - ------- - Any - The result returned by ``func`` on the first successful attempt. - - Raises - ------ - TypeError - If ``func`` is not an async function. Use ``retry_call`` instead. - Exception - Any exception raised by ``func`` when retries are disabled. - retry_exceptions - One of the configured ``retry_exceptions`` if all retry attempts are - exhausted while retries are enabled. - Exception - Any exception not matching ``retry_exceptions`` is propagated - immediately without retry. - - Examples - -------- - Basic usage with environment-controlled configuration:: - - async def fetch(): - ... - - result = await async_retry_call(fetch) - - Overriding retry configuration and the set of retryable exceptions:: - - async def fetch_with_timeout(): - ... - - result = await async_retry_call( - fetch_with_timeout, - retry_exceptions=(OSError, TimeoutError, ConnectionError), - max_attempts=5, - base_delay=0.5, - max_delay=10.0, - ) + """Asynchronous retry wrapper with optional exponential backoff. + + Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. + When disabled, the function executes exactly once. + + Args: + func: The async callable to be executed and potentially retried. + *args: Positional arguments forwarded to func on each attempt. + retry_exceptions: Exception type(s) that trigger a retry. Any exception + not matching these types is raised immediately. Defaults to + (OSError, TimeoutError). + max_attempts: Maximum number of attempts. If None, uses + BT_RETRY_MAX_ATTEMPTS environment variable (default: 3). + base_delay: Base delay in seconds for exponential backoff. If None, + uses BT_RETRY_BASE_DELAY environment variable (default: 1.0). + max_delay: Maximum delay in seconds between attempts. If None, uses + BT_RETRY_MAX_DELAY environment variable (default: 60.0). + **kwargs: Keyword arguments forwarded to func on each attempt. + + Returns: + The result from the first successful func execution. + + Raises: + TypeError: If func is not an async function. Use retry_call instead. + Exception: Any exception raised by func when retries are disabled, or + when the exception type doesn't match retry_exceptions, or after + all retry attempts are exhausted. """ # Validate that func is async if not inspect.iscoroutinefunction(func): raise TypeError( - f"async_retry_call() requires an async function. " + f"async_retry_call() requires an async function. " f"Use retry_call() instead for {func.__name__}." ) @@ -370,9 +284,9 @@ async def fetch_with_timeout(): try: return await func(*args, **kwargs) except retry_exceptions as e: - if attempt == _max_attempts: + if attempt == _max_attempts: logger.debug( - f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) raise From 4258910cdbcdac367d906a80cfbeb5ce28d16468 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:38:38 -0800 Subject: [PATCH 26/40] Refactor retry.py documentation and logging messages --- bittensor/utils/retry.py | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index ca6bb88a03..11754dc555 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -1,19 +1,19 @@ -"""Retry utilities for handling transient failures with exponential backoff. +"""Retry utilities for handling transient failures with exponential backoff. This module provides optional retry wrappers for both synchronous and asynchronous functions. Retry behavior is controlled via environment variables and is disabled by default. Environment Variables: - BT_RETRY_ENABLED: Enable retry behavior ("true", "1", "yes", "on") - BT_RETRY_MAX_ATTEMPTS: Maximum retry attempts (default: 3) - BT_RETRY_BASE_DELAY: Base delay in seconds (default: 1.0) - BT_RETRY_MAX_DELAY: Maximum delay in seconds (default: 60.0) + BT_RETRY_ENABLED: Enable retry behavior ("true", "1", "yes", "on") + BT_RETRY_MAX_ATTEMPTS: Maximum retry attempts (default: 3) + BT_RETRY_BASE_DELAY: Base delay in seconds (default: 1.0) + BT_RETRY_MAX_DELAY: Maximum delay in seconds (default: 60.0) BT_RETRY_BACKOFF_FACTOR: Exponential backoff multiplier (default: 2.0) -Note: +Note: This utility is not used internally by the SDK. It is provided as an - optional helper for users who wish to implement consistent retry behavior. + optional helper for users who wish to implement consistent retry behavior. For more information on retry strategies, see: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ @@ -94,9 +94,9 @@ def _retry_max_delay() -> float: """Get the maximum delay (in seconds) for retries from the environment, with validation.""" default = 60.0 raw = os.environ.get("BT_RETRY_MAX_DELAY") - if raw is None or raw == "": + if raw is None or raw == "": return default - try: + try: value = float(raw) if value < 0: logger.warning( @@ -136,7 +136,7 @@ def _retry_backoff_factor() -> float: return value except (TypeError, ValueError): logger.warning( - "Invalid value for BT_RETRY_BACKOFF_FACTOR=%r; falling back to default %.2f", + "Invalid value for BT_RETRY_BACKOFF_FACTOR=%r (must be positive); falling back to default %.2f", raw, default, ) @@ -158,20 +158,20 @@ def retry_call( TimeoutError, ), max_attempts: Optional[int] = None, - base_delay: Optional[float] = None, + base_delay: Optional[float] = None, max_delay: Optional[float] = None, **kwargs, ) -> Any: """Synchronous retry wrapper with optional exponential backoff. - Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. - When disabled, the function executes exactly once. + Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. + When disabled, the function executes exactly once. Args: - func: The callable to be executed and potentially retried. + func: The callable to be executed and potentially retried. *args: Positional arguments forwarded to func. - retry_exceptions: Exception type(s) that trigger a retry. Any exception - not matching these types is raised immediately. Defaults to + retry_exceptions: Exception type(s) that trigger a retry. Any exception + not matching these types is raised immediately. Defaults to (OSError, TimeoutError). max_attempts: Maximum number of attempts. If None, uses BT_RETRY_MAX_ATTEMPTS environment variable (default: 3). @@ -179,21 +179,21 @@ def retry_call( uses BT_RETRY_BASE_DELAY environment variable (default: 1.0). max_delay: Maximum delay in seconds between attempts. If None, uses BT_RETRY_MAX_DELAY environment variable (default: 60.0). - **kwargs: Keyword arguments forwarded to func. + **kwargs: Keyword arguments forwarded to func. Returns: - The return value from the first successful func execution. + The return value from the first successful func execution. Raises: - TypeError: If func is an async function. Use async_retry_call instead. + TypeError: If func is an async function. Use async_retry_call instead. Exception: Any exception raised by func when retries are disabled, or when the exception type doesn't match retry_exceptions, or after - all retry attempts are exhausted. + all retry attempts are exhausted. """ # Validate that func is not async if inspect.iscoroutinefunction(func): raise TypeError( - f"retry_call() cannot be used with async functions. " + f"retry_call() cannot be used with async functions. " f"Use async_retry_call() instead for {func.__name__}." ) @@ -211,13 +211,13 @@ def retry_call( except retry_exceptions as e: if attempt == _max_attempts: logger.debug( - f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) raise backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) logger.debug( - f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." + f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:. 2f}s..." ) time.sleep(backoff) @@ -239,13 +239,13 @@ async def async_retry_call( ) -> Any: """Asynchronous retry wrapper with optional exponential backoff. - Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. + Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. When disabled, the function executes exactly once. Args: - func: The async callable to be executed and potentially retried. - *args: Positional arguments forwarded to func on each attempt. - retry_exceptions: Exception type(s) that trigger a retry. Any exception + func: The async callable to be executed and potentially retried. + *args: Positional arguments forwarded to func on each attempt. + retry_exceptions: Exception type(s) that trigger a retry. Any exception not matching these types is raised immediately. Defaults to (OSError, TimeoutError). max_attempts: Maximum number of attempts. If None, uses @@ -253,7 +253,7 @@ async def async_retry_call( base_delay: Base delay in seconds for exponential backoff. If None, uses BT_RETRY_BASE_DELAY environment variable (default: 1.0). max_delay: Maximum delay in seconds between attempts. If None, uses - BT_RETRY_MAX_DELAY environment variable (default: 60.0). + BT_RETRY_MAX_DELAY environment variable (default: 60.0). **kwargs: Keyword arguments forwarded to func on each attempt. Returns: @@ -283,10 +283,10 @@ async def async_retry_call( for attempt in range(1, _max_attempts + 1): try: return await func(*args, **kwargs) - except retry_exceptions as e: - if attempt == _max_attempts: + except retry_exceptions as e: + if attempt == _max_attempts: logger.debug( - f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) raise From ff359d4c6543a29309a68c66adc5d2c6338c3b67 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:40:28 -0800 Subject: [PATCH 27/40] Update mock call parameters for clarity --- tests/unit_tests/test_async_subtensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 0f83110729..67ebc7dc41 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -570,10 +570,10 @@ async def test_query_runtime_api(subtensor, mocker): # Asserts mocked_runtime_call.assert_called_once_with( - fake_runtime_api, - fake_method, - fake_params, - fake_block_hash, + api=fake_runtime_api, + method=fake_method, + params=fake_params, + block_hash=fake_block_hash, ) assert result == mocked_runtime_call.return_value.value From 772ab8ec0df93c3200d26f3b37f1f32d55135964 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:49:11 -0800 Subject: [PATCH 28/40] Fix formatting and spacing in retry.py --- bittensor/utils/retry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 11754dc555..71edb4338e 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -151,7 +151,7 @@ def _get_backoff_time(attempt: int, base_delay: float, max_delay: float) -> floa def retry_call( - func: Callable, + func: Callable, *args, retry_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = ( OSError, @@ -211,7 +211,7 @@ def retry_call( except retry_exceptions as e: if attempt == _max_attempts: logger.debug( - f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" + f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) raise @@ -282,7 +282,7 @@ async def async_retry_call( for attempt in range(1, _max_attempts + 1): try: - return await func(*args, **kwargs) + return await func(*args, **kwargs) except retry_exceptions as e: if attempt == _max_attempts: logger.debug( From df2652e2426d6668591d56ed1cb6d71046c82465 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 12:54:52 -0800 Subject: [PATCH 29/40] Change function call to await for async support --- bittensor/utils/retry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 71edb4338e..28dca20096 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -207,9 +207,9 @@ def retry_call( for attempt in range(1, _max_attempts + 1): try: - return func(*args, **kwargs) + return await func(*args, **kwargs) except retry_exceptions as e: - if attempt == _max_attempts: + if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) From bf7b37a2ca94d0a3f439b94e13d5ba5a3816996f Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Wed, 31 Dec 2025 07:22:50 -0800 Subject: [PATCH 30/40] Apply Ruff formatting --- bittensor/core/async_subtensor.py | 14 ++++++++------ bittensor/core/subtensor.py | 3 ++- bittensor/utils/retry.py | 26 +++++++++++++------------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index c54eac656a..92e1518508 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -965,12 +965,14 @@ async def query_runtime_api( block_hash = await self.determine_block_hash(block, block_hash, reuse_block) if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - return (await self.substrate.runtime_call( - api=runtime_api, - method=method, - params=params, - block_hash=block_hash, - )).value + return ( + await self.substrate.runtime_call( + api=runtime_api, + method=method, + params=params, + block_hash=block_hash, + ) + ).value async def query_subtensor( self, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 124fbf9487..27c6ae23c0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -169,6 +169,7 @@ from bittensor_wallet import Keypair, Wallet from scalecodec.types import GenericCall + class Subtensor(SubtensorMixin): """Synchronous interface for interacting with the Bittensor blockchain. @@ -569,7 +570,7 @@ def get_hyperparameter( block_hash=block_hash, ) return getattr(result, "value", result) - + @property def block(self) -> int: """Provides an asynchronous getter to retrieve the current block number. diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 28dca20096..259b570567 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -1,8 +1,8 @@ -"""Retry utilities for handling transient failures with exponential backoff. +"""Retry utilities for handling transient failures with exponential backoff. This module provides optional retry wrappers for both synchronous and asynchronous functions. Retry behavior is controlled via environment variables and is disabled -by default. +by default. Environment Variables: BT_RETRY_ENABLED: Enable retry behavior ("true", "1", "yes", "on") @@ -11,9 +11,9 @@ BT_RETRY_MAX_DELAY: Maximum delay in seconds (default: 60.0) BT_RETRY_BACKOFF_FACTOR: Exponential backoff multiplier (default: 2.0) -Note: +Note: This utility is not used internally by the SDK. It is provided as an - optional helper for users who wish to implement consistent retry behavior. + optional helper for users who wish to implement consistent retry behavior. For more information on retry strategies, see: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ @@ -94,9 +94,9 @@ def _retry_max_delay() -> float: """Get the maximum delay (in seconds) for retries from the environment, with validation.""" default = 60.0 raw = os.environ.get("BT_RETRY_MAX_DELAY") - if raw is None or raw == "": + if raw is None or raw == "": return default - try: + try: value = float(raw) if value < 0: logger.warning( @@ -164,8 +164,8 @@ def retry_call( ) -> Any: """Synchronous retry wrapper with optional exponential backoff. - Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. - When disabled, the function executes exactly once. + Retries are only enabled when BT_RETRY_ENABLED is set to a truthy value. + When disabled, the function executes exactly once. Args: func: The callable to be executed and potentially retried. @@ -179,10 +179,10 @@ def retry_call( uses BT_RETRY_BASE_DELAY environment variable (default: 1.0). max_delay: Maximum delay in seconds between attempts. If None, uses BT_RETRY_MAX_DELAY environment variable (default: 60.0). - **kwargs: Keyword arguments forwarded to func. + **kwargs: Keyword arguments forwarded to func. Returns: - The return value from the first successful func execution. + The return value from the first successful func execution. Raises: TypeError: If func is an async function. Use async_retry_call instead. @@ -209,7 +209,7 @@ def retry_call( try: return await func(*args, **kwargs) except retry_exceptions as e: - if attempt == _max_attempts: + if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" ) @@ -282,8 +282,8 @@ async def async_retry_call( for attempt in range(1, _max_attempts + 1): try: - return await func(*args, **kwargs) - except retry_exceptions as e: + return await func(*args, **kwargs) + except retry_exceptions as e: if attempt == _max_attempts: logger.debug( f"Retry exhausted after {_max_attempts} attempts. Last error: {e}" From 5b05b8638d0f917b88028cb3f68c0f63e9f7297b Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 22:50:33 -0800 Subject: [PATCH 31/40] Refactor runtime_call assertions in unit tests --- tests/unit_tests/test_async_subtensor.py | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 67ebc7dc41..b6030714f4 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1068,13 +1068,11 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): ) subtensor.substrate.runtime_call.assert_awaited_once() subtensor.substrate.runtime_call.assert_called_once_with( - "NeuronInfoRuntimeApi", - "get_neuron", - [fake_netuid, fake_uid.value], - None, + api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[fake_netuid, fake_uid.value], + block_hash=None, ) - mocked_neuron_info.assert_called_once_with(fake_result) - assert result == "fake_neuron_info" @pytest.mark.asyncio @@ -1146,10 +1144,10 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock reuse_block_hash=False, ) subtensor.substrate.runtime_call.assert_called_once_with( - "NeuronInfoRuntimeApi", - "get_neuron", - [fake_netuid, fake_uid], - None, + api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[fake_netuid, fake_uid], + block_hash=None, ) mocked_get_null_neuron.assert_called_once() assert result == "null_neuron" @@ -1349,11 +1347,11 @@ async def test_get_delegated_with_empty_result(subtensor, mocker): result = await subtensor.get_delegated(coldkey_ss58=fake_coldkey_ss58) # Asserts - mocked_runtime_call.assert_called_once_with( - "DelegateInfoRuntimeApi", - "get_delegated", - [fake_coldkey_ss58], - None, + mocked_runtime_call.assert_called_once_with( + api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[fake_coldkey_ss58], + block_hash=None, ) assert result == [] From fe38894d922d0d6846bb0781d47ae95a73231f09 Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 22:59:24 -0800 Subject: [PATCH 32/40] Fix indentation in async subtensor test --- tests/unit_tests/test_async_subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index b6030714f4..f371f3b946 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1347,7 +1347,7 @@ async def test_get_delegated_with_empty_result(subtensor, mocker): result = await subtensor.get_delegated(coldkey_ss58=fake_coldkey_ss58) # Asserts - mocked_runtime_call.assert_called_once_with( + mocked_runtime_call.assert_called_once_with( api="DelegateInfoRuntimeApi", method="get_delegated", params=[fake_coldkey_ss58], From 0cba2a77b1347d22298e33a7a42e3f99ed59e52f Mon Sep 17 00:00:00 2001 From: Dairus Date: Tue, 30 Dec 2025 23:13:52 -0800 Subject: [PATCH 33/40] Add pytest configuration to pyproject.toml --- pyproject.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 173aa491d6..e42d0d51b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,3 +94,14 @@ classifiers = [ [tool.setuptools] package-dir = {"bittensor" = "bittensor"} script-files = ["bittensor/utils/certifi.sh"] + +[tool.pytest. ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +filterwarnings = [ + "ignore::DeprecationWarning:pkg_resources.*:", +] +asyncio_default_fixture_loop_scope = "session" +addopts = "-s" From e32aa3ac6b95cd1a12b2cfb2313888941396a42c Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Wed, 31 Dec 2025 08:39:32 -0800 Subject: [PATCH 34/40] Fix import error and remove await from sync function --- bittensor/utils/retry.py | 2 +- tests/unit_tests/utils/test_retry.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 259b570567..9780c6afd1 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -207,7 +207,7 @@ def retry_call( for attempt in range(1, _max_attempts + 1): try: - return await func(*args, **kwargs) + return func(*args, **kwargs) except retry_exceptions as e: if attempt == _max_attempts: logger.debug( diff --git a/tests/unit_tests/utils/test_retry.py b/tests/unit_tests/utils/test_retry.py index 00320bbfd3..30a8d673ab 100644 --- a/tests/unit_tests/utils/test_retry.py +++ b/tests/unit_tests/utils/test_retry.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import Mock, patch, AsyncMock -from bittensor.utils.retry import retry_call, retry_async +from bittensor.utils.retry import retry_call, async_retry_call # Create custom exception for testing class NetworkError(Exception): @@ -76,14 +76,14 @@ def test_sync_default_retry_exceptions_do_not_retry_non_network_error(enable_ret @pytest.mark.asyncio async def test_async_retry_success(enable_retries): mock_func = AsyncMock(return_value="success") - result = await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + result = await async_retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) assert result == "success" assert mock_func.call_count == 1 @pytest.mark.asyncio async def test_async_retry_eventual_success(enable_retries, mock_async_sleep): mock_func = AsyncMock(side_effect=[NetworkError("Fail 1"), NetworkError("Fail 2"), "success"]) - result = await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + result = await async_retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) assert result == "success" assert mock_func.call_count == 3 @@ -91,20 +91,20 @@ async def test_async_retry_eventual_success(enable_retries, mock_async_sleep): async def test_async_retry_exhaustion(enable_retries, mock_async_sleep): mock_func = AsyncMock(side_effect=NetworkError("Persistent Fail")) with pytest.raises(NetworkError, match="Persistent Fail"): - await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + await async_retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) assert mock_func.call_count == 3 @pytest.mark.asyncio async def test_async_no_retry_on_wrong_exception(enable_retries): mock_func = AsyncMock(side_effect=NonRetryableError("Fatal")) with pytest.raises(NonRetryableError, match="Fatal"): - await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + await async_retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) assert mock_func.call_count == 1 @pytest.mark.asyncio async def test_async_disabled_retries_executes_once(disable_retries): mock_func = AsyncMock(side_effect=NetworkError("Fail")) with pytest.raises(NetworkError, match="Fail"): - await retry_async(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) + await async_retry_call(mock_func, retry_exceptions=(NetworkError,), max_attempts=3) assert mock_func.call_count == 1 From b81de9acdea02b098843006d514829ad8728e8b8 Mon Sep 17 00:00:00 2001 From: Dairus01 Date: Wed, 31 Dec 2025 08:51:49 -0800 Subject: [PATCH 35/40] Fix format specifier typos in retry.py --- bittensor/utils/retry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 9780c6afd1..4d26059292 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -75,7 +75,7 @@ def _retry_base_delay() -> float: value = float(raw) if value < 0: logger.warning( - "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %. 2f", + "Invalid value for BT_RETRY_BASE_DELAY=%r (must be non-negative); falling back to default %.2f", raw, default, ) @@ -217,7 +217,7 @@ def retry_call( backoff = _get_backoff_time(attempt - 1, _base_delay, _max_delay) logger.debug( - f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:. 2f}s..." + f"Retry attempt {attempt}/{_max_attempts} failed with {e}. Retrying in {backoff:.2f}s..." ) time.sleep(backoff) From b97e8290757718b686a4ba0ae06620a3259b53f3 Mon Sep 17 00:00:00 2001 From: Dairus Date: Sun, 4 Jan 2026 21:28:53 +0100 Subject: [PATCH 36/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 4d26059292..7871011eba 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -12,8 +12,9 @@ BT_RETRY_BACKOFF_FACTOR: Exponential backoff multiplier (default: 2.0) Note: - This utility is not used internally by the SDK. It is provided as an - optional helper for users who wish to implement consistent retry behavior. + This utility may be used internally by the SDK at outbound network boundaries + (e.g., Dendrite and Subtensor) and is also provided as an optional helper for + users who wish to implement consistent retry behavior. For more information on retry strategies, see: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ From 50ec68936ce00e66d6cbfa4a4abec36f7f008251 Mon Sep 17 00:00:00 2001 From: Dairus Date: Sun, 4 Jan 2026 21:31:35 +0100 Subject: [PATCH 37/40] Update pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e42d0d51b7..df898fca1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ classifiers = [ package-dir = {"bittensor" = "bittensor"} script-files = ["bittensor/utils/certifi.sh"] -[tool.pytest. ini_options] +[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] From 739d86815093a994596b7fe3370c5ceb72ff1c25 Mon Sep 17 00:00:00 2001 From: Dairus Date: Sun, 4 Jan 2026 21:31:55 +0100 Subject: [PATCH 38/40] Update bittensor/utils/retry.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- bittensor/utils/retry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/retry.py b/bittensor/utils/retry.py index 7871011eba..7da60ba98f 100644 --- a/bittensor/utils/retry.py +++ b/bittensor/utils/retry.py @@ -28,7 +28,7 @@ import logging from typing import Type, Tuple, Optional, Callable, Any, Union -logger = logging.getLogger("bittensor. utils.retry") +logger = logging.getLogger("bittensor.utils.retry") # Helpers for runtime environment variable access From da72ed3b0aafdadb2ee5d0a4da62d5957c711f13 Mon Sep 17 00:00:00 2001 From: Roman <167799377+basfroman@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:24:54 -0800 Subject: [PATCH 39/40] Update bittensor/core/subtensor.py --- bittensor/core/subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 27c6ae23c0..a17628a277 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -569,6 +569,7 @@ def get_hyperparameter( params=[netuid], block_hash=block_hash, ) + return getattr(result, "value", result) @property From 6d57121675aabad784a09483eb81a2ba85e88f78 Mon Sep 17 00:00:00 2001 From: Roman <167799377+basfroman@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:25:03 -0800 Subject: [PATCH 40/40] Update bittensor/core/dendrite.py --- bittensor/core/dendrite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index d8a7c002af..e559a1a026 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -8,6 +8,7 @@ import aiohttp from bittensor_wallet import Keypair, Wallet + from bittensor.core.axon import Axon from bittensor.core.chain_data import AxonInfo from bittensor.core.settings import version_as_int