diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 75d3a7f80d..0f96f59c99 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -81,6 +81,16 @@ def turn_console_off(): __local_entrypoint__ = "ws://127.0.0.1:9945" +# Block Explorers map network to explorer url +## Must all be polkadotjs explorer urls +__network_explorer_map__ = { + 'local': "https://explorer.nakamoto.opentensor.ai/#/explorer", + 'nakamoto': "https://explorer.nakamoto.opentensor.ai/#/explorer", + 'endpoint': "https://explorer.nakamoto.opentensor.ai/#/", + 'nobunaga': "https://staging.opentensor.ai/#/explorer", + 'finney': "https://polkadot.js.org/apps/?rpc=wss://staging.parachain.opentensor.ai#/explorer" +} + # Avoid collisions with other processes from .utils.test_utils import get_random_unused_port mock_subtensor_port = get_random_unused_port() diff --git a/bittensor/_subtensor/extrinsics/transfer.py b/bittensor/_subtensor/extrinsics/transfer.py index c82f0ba3f0..6e355d29b5 100644 --- a/bittensor/_subtensor/extrinsics/transfer.py +++ b/bittensor/_subtensor/extrinsics/transfer.py @@ -128,8 +128,11 @@ def transfer_extrinsic( bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") block_hash = response.block_hash bittensor.__console__.print("[green]Block Hash: {}[/green]".format( block_hash )) - explorer_url = "https://explorer.nakamoto.opentensor.ai/#/explorer/query/{block_hash}".format( block_hash = block_hash ) - bittensor.__console__.print("[green]Explorer Link: {}[/green]".format( explorer_url )) + + explorer_url = bittensor.utils.get_explorer_url_for_network( subtensor.network, block_hash ) + if explorer_url is not None: + bittensor.__console__.print("[green]Explorer Link: {}[/green]".format( explorer_url )) + else: bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index b288804592..509c717f66 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -281,6 +281,8 @@ def unstake_multiple ( """ Removes stake from each hotkey_ss58 in the list, using each amount, to a common coldkey. """ return unstake_multiple_extrinsic( self, wallet, hotkey_ss58s, amounts, wait_for_inclusion, wait_for_finalization, prompt) + + def unstake ( self, wallet: 'bittensor.wallet', diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 0f03e70cf0..ddaf11d18a 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -193,6 +193,48 @@ def strtobool(val: str) -> bool: else: raise ValueError("invalid truth value %r" % (val,)) +def get_explorer_root_url_by_network_from_map(network: str, network_map: Dict[str, str]) -> Optional[str]: + r""" + Returns the explorer root url for the given network name from the given network map. + + Args: + network(str): The network to get the explorer url for. + network_map(Dict[str, str]): The network map to get the explorer url from. + + Returns: + The explorer url for the given network. + Or None if the network is not in the network map. + """ + explorer_url: Optional[str] = None + if network in network_map: + explorer_url = network_map[network] + + return explorer_url + + +def get_explorer_url_for_network(network: str, block_hash: str) -> Optional[str]: + r""" + Returns the explorer url for the given block hash and network. + + Args: + network(str): The network to get the explorer url for. + block_hash(str): The block hash to get the explorer url for. + + Returns: + The explorer url for the given block hash and network. + Or None if the network is not known. + """ + + explorer_url: Optional[str] = None + # Will be None if the network is not known. i.e. not in bittensor.__network_explorer_map__ + explorer_root_url: Optional[str] = get_explorer_root_url_by_network_from_map(network, bittensor.__network_explorer_map__) + + if explorer_root_url is not None: + # We are on a known network. + explorer_url = "{root_url}/query/{block_hash}".format( root_url=explorer_root_url, block_hash = block_hash ) + + return explorer_url + def ss58_address_to_bytes(ss58_address: str) -> bytes: """Converts a ss58 address to a bytes object.""" account_id_hex: str = scalecodec.ss58_decode(ss58_address, bittensor.__ss58_format__) @@ -213,4 +255,3 @@ def u8_key_to_ss58(u8_key: List[int]) -> str: """ # First byte is length, then 32 bytes of key. return scalecodec.ss58_encode( bytes(u8_key).hex(), bittensor.__ss58_format__) - \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 2ababe54ae..9dbb9ecfe1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,4 +4,5 @@ pytest-xdist==3.0.2 pytest-rerunfailures==10.2 coveralls==3.3.1 pytest-cov==4.0.0 -codecov==2.1.12 \ No newline at end of file +codecov==2.1.12 +ddt==1.6.0 \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/utils/test_utils.py b/tests/unit_tests/bittensor_tests/utils/test_utils.py index f64951f5e1..d7c42b70e3 100644 --- a/tests/unit_tests/bittensor_tests/utils/test_utils.py +++ b/tests/unit_tests/bittensor_tests/utils/test_utils.py @@ -10,16 +10,21 @@ import unittest from sys import platform from types import SimpleNamespace +from typing import Dict from unittest.mock import MagicMock, patch -import bittensor import pytest -import torch from _pytest.fixtures import fixture -from bittensor.utils import CUDASolver + +from ddt import data, ddt, unpack + +import torch from loguru import logger from substrateinterface.base import Keypair +import bittensor +from bittensor.utils import CUDASolver + @fixture(scope="function") def setup_chain(): @@ -546,6 +551,42 @@ class MockException(Exception): ## Should incerase by the number of nonces tried == TPB * update_interval self.assertEqual(nonce_start_after_iteration, (initial_nonce_start + update_interval * TPB) % nonce_limit, "nonce_start was not updated by the correct amount") +@ddt +class TestExplorerURL(unittest.TestCase): + network_map: Dict[str, str] = { + "nakamoto": "https://polkadot.js.org/apps/?rpc=wss://archivelb.nakamoto.opentensor.ai:9943#/explorer", + "example": "https://polkadot.js.org/apps/?rpc=wss://example.example.com#/explorer", + "nobunaga": "https://polkadot.js.org/apps/?rpc=wss://nobunaga.bittensor.com:9943#/explorer", + # "bad": None # no explorer for this network + } + + @data( + ("nobunaga", "https://polkadot.js.org/apps/?rpc=wss://nobunaga.bittensor.com:9943#/explorer"), + ("nakamoto", "https://polkadot.js.org/apps/?rpc=wss://archivelb.nakamoto.opentensor.ai:9943#/explorer"), + ("example", "https://polkadot.js.org/apps/?rpc=wss://example.example.com#/explorer"), + ("bad", None), + ("", None), + ("networknamewithoutexplorer", None) + ) + @unpack + def test_get_explorer_root_url_by_network_from_map(self, network: str, expected: str) -> str: + self.assertEqual(bittensor.utils.get_explorer_root_url_by_network_from_map(network, self.network_map), expected) + + @data( + ("nobunaga", "0x123", "https://polkadot.js.org/apps/?rpc=wss://nobunaga.bittensor.com:9943#/explorer/query/0x123"), + ("example", "0x123", "https://polkadot.js.org/apps/?rpc=wss://example.example.com#/explorer/query/0x123"), + ("bad", "0x123", None), + ("", "0x123", None), + ("networknamewithoutexplorer", "0x123", None) + ) + @unpack + def test_get_explorer_url_for_network_by_network_and_block_hash(self, network: str, block_hash: str, expected: str) -> str: + def override_map_func(network: str, _) -> str: + return self.network_map[network] + + with patch('bittensor.utils.get_explorer_root_url_by_network_from_map', side_effect=override_map_func): + self.assertEqual(bittensor.utils.get_explorer_url_for_network(network, block_hash), expected) + if __name__ == "__main__": unittest.main()