From 7212a46193d20a7b4a654b9b9fb2bc175f7fc756 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 25 Sep 2024 16:29:03 +0200 Subject: [PATCH 001/332] Initial commit --- bittensor_cli/src/__init__.py | 178 ++++++++++++++++++++-- bittensor_cli/src/bittensor/chain_data.py | 136 +++++++++++++++++ 2 files changed, 303 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a5a87e16..6df30907 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -4,16 +4,20 @@ class Constants: - networks = ["local", "finney", "test", "archive"] + networks = ["local", "finney", "test", "archive", "rao", "dev"] finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443" archive_entrypoint = "wss://archive.chain.opentensor.ai:443" + rao_entrypoint = "wss://rao.chain.opentensor.ai:443/" + dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " local_entrypoint = "ws://127.0.0.1:9444" network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, "archive": archive_entrypoint, "local": local_entrypoint, + "dev": dev_entrypoint, + "rao": rao_entrypoint } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" @@ -206,24 +210,34 @@ class WalletValidationTypes(Enum): "StakeInfoRuntimeApi": { "methods": { "get_stake_info_for_coldkey": { + "params": [{"name": "coldkey_account_vec", "type": "Vec"}], + "type": "Vec", + }, + "get_stake_info_for_coldkeys": { "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, + {"name": "coldkey_account_vecs", "type": "Vec>"} ], "type": "Vec", }, - "get_stake_info_for_coldkeys": { + "get_subnet_stake_info_for_coldkeys": { "params": [ - { - "name": "coldkey_account_vecs", - "type": "Vec>", - }, + {"name": "coldkey_account_vecs", "type": "Vec>"}, + {"name": "netuid", "type": "u16"}, ], "type": "Vec", }, - }, + "get_subnet_stake_info_for_coldkey": { + "params": [ + {"name": "coldkey_account_vec", "type": "Vec"}, + {"name": "netuid", "type": "u16"}, + ], + "type": "Vec", + }, + "get_total_subnet_stake": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, + } }, "ValidatorIPRuntimeApi": { "methods": { @@ -301,6 +315,148 @@ class WalletValidationTypes(Enum): }, } +UNITS = [ + "\u03c4", # τ (tau, 0) + "\u03b1", # α (alpha, 1) + "\u03b2", # β (beta, 2) + "\u03b3", # γ (gamma, 3) + "\u03b4", # δ (delta, 4) + "\u03b5", # ε (epsilon, 5) + "\u03b6", # ζ (zeta, 6) + "\u03b7", # η (eta, 7) + "\u03b8", # θ (theta, 8) + "\u03b9", # ι (iota, 9) + "\u03ba", # κ (kappa, 10) + "\u03bb", # λ (lambda, 11) + "\u03bc", # μ (mu, 12) + "\u03bd", # ν (nu, 13) + "\u03be", # ξ (xi, 14) + "\u03bf", # ο (omicron, 15) + "\u03c0", # π (pi, 16) + "\u03c1", # ρ (rho, 17) + "\u03c3", # σ (sigma, 18) + "t", # t (tau, 19) + "\u03c5", # υ (upsilon, 20) + "\u03c6", # φ (phi, 21) + "\u03c7", # χ (chi, 22) + "\u03c8", # ψ (psi, 23) + "\u03c9", # ω (omega, 24) + # Hebrew letters + "\u05d0", # א (aleph, 25) + "\u05d1", # ב (bet, 26) + "\u05d2", # ג (gimel, 27) + "\u05d3", # ד (dalet, 28) + "\u05d4", # ה (he, 29) + "\u05d5", # ו (vav, 30) + "\u05d6", # ז (zayin, 31) + "\u05d7", # ח (het, 32) + "\u05d8", # ט (tet, 33) + "\u05d9", # י (yod, 34) + "\u05da", # ך (final kaf, 35) + "\u05db", # כ (kaf, 36) + "\u05dc", # ל (lamed, 37) + "\u05dd", # ם (final mem, 38) + "\u05de", # מ (mem, 39) + "\u05df", # ן (final nun, 40) + "\u05e0", # נ (nun, 41) + "\u05e1", # ס (samekh, 42) + "\u05e2", # ע (ayin, 43) + "\u05e3", # ף (final pe, 44) + "\u05e4", # פ (pe, 45) + "\u05e5", # ץ (final tsadi, 46) + "\u05e6", # צ (tsadi, 47) + "\u05e7", # ק (qof, 48) + "\u05e8", # ר (resh, 49) + "\u05e9", # ש (shin, 50) + "\u05ea", # ת (tav, 51) + # Georgian Alphabet (Mkhedruli) + "\u10d0", # ა (Ani, 97) + "\u10d1", # ბ (Bani, 98) + "\u10d2", # გ (Gani, 99) + "\u10d3", # დ (Doni, 100) + "\u10d4", # ე (Eni, 101) + "\u10d5", # ვ (Vini, 102) + # Armenian Alphabet + "\u0531", # Ա (Ayp, 103) + "\u0532", # Բ (Ben, 104) + "\u0533", # Գ (Gim, 105) + "\u0534", # Դ (Da, 106) + "\u0535", # Ե (Ech, 107) + "\u0536", # Զ (Za, 108) + # "\u055e", # ՞ (Question mark, 109) + # Runic Alphabet + "\u16a0", # ᚠ (Fehu, wealth, 81) + "\u16a2", # ᚢ (Uruz, strength, 82) + "\u16a6", # ᚦ (Thurisaz, giant, 83) + "\u16a8", # ᚨ (Ansuz, god, 84) + "\u16b1", # ᚱ (Raidho, ride, 85) + "\u16b3", # ᚲ (Kaunan, ulcer, 86) + "\u16c7", # ᛇ (Eihwaz, yew, 87) + "\u16c9", # ᛉ (Algiz, protection, 88) + "\u16d2", # ᛒ (Berkanan, birch, 89) + # Cyrillic Alphabet + "\u0400", # Ѐ (Ie with grave, 110) + "\u0401", # Ё (Io, 111) + "\u0402", # Ђ (Dje, 112) + "\u0403", # Ѓ (Gje, 113) + "\u0404", # Є (Ukrainian Ie, 114) + "\u0405", # Ѕ (Dze, 115) + # Coptic Alphabet + "\u2c80", # Ⲁ (Alfa, 116) + "\u2c81", # ⲁ (Small Alfa, 117) + "\u2c82", # Ⲃ (Vida, 118) + "\u2c83", # ⲃ (Small Vida, 119) + "\u2c84", # Ⲅ (Gamma, 120) + "\u2c85", # ⲅ (Small Gamma, 121) + # Arabic letters + "\u0627", # ا (alef, 52) + "\u0628", # ب (ba, 53) + "\u062a", # ت (ta, 54) + "\u062b", # ث (tha, 55) + "\u062c", # ج (jeem, 56) + "\u062d", # ح (ha, 57) + "\u062e", # خ (kha, 58) + "\u062f", # د (dal, 59) + "\u0630", # ذ (dhal, 60) + "\u0631", # ر (ra, 61) + "\u0632", # ز (zay, 62) + "\u0633", # س (seen, 63) + "\u0634", # ش (sheen, 64) + "\u0635", # ص (sad, 65) + "\u0636", # ض (dad, 66) + "\u0637", # ط (ta, 67) + "\u0638", # ظ (dha, 68) + "\u0639", # ع (ain, 69) + "\u063a", # غ (ghain, 70) + "\u0641", # ف (fa, 71) + "\u0642", # ق (qaf, 72) + "\u0643", # ك (kaf, 73) + "\u0644", # ل (lam, 74) + "\u0645", # م (meem, 75) + "\u0646", # ن (noon, 76) + "\u0647", # ه (ha, 77) + "\u0648", # و (waw, 78) + "\u0649", # ى (alef maksura, 79) + "\u064a", # ي (ya, 80) + # Ogham Alphabet + "\u1680", #   (Space, 90) + "\u1681", # ᚁ (Beith, birch, 91) + "\u1682", # ᚂ (Luis, rowan, 92) + "\u1683", # ᚃ (Fearn, alder, 93) + "\u1684", # ᚄ (Sail, willow, 94) + "\u1685", # ᚅ (Nion, ash, 95) + "\u169b", # ᚛ (Forfeda, 96) + # Brahmi Script TODO verify these https://discord.com/channels/799672011265015819/1176889593136693339/1288500713625878558 + "\u11000", # 𑀀 (A, 122) + "\u11001", # 𑀁 (Aa, 123) + "\u11002", # 𑀂 (I, 124) + "\u11003", # 𑀃 (Ii, 125) + "\u11005", # 𑀅 (U, 126) + # Tifinagh Alphabet + "\u2d30", # ⴰ (Ya, 127) + "\u2d31", # ⴱ (Yab, 128) +] + NETWORK_EXPLORER_MAP = { "opentensor": { "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 73f41b1f..b316d8e2 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -571,6 +571,47 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ) return result +@dataclass +class SubnetInfoV2: + """Dataclass for subnet info.""" + netuid: int + owner_ss58: str + max_allowed_validators: int + scaling_law_power: float + subnetwork_n: int + max_n: int + blocks_since_epoch: int + modality: int + emission_value: float + burn: Balance + tao_locked: Balance + hyperparameters: "SubnetHyperparameters" + dynamic_pool: "DynamicPool" + + @classmethod + def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetInfoV2"]: + """Returns a SubnetInfoV2 object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + decoded = bt_decode.SubnetInfoV2.decode(vec_u8) # TODO fix values + + if decoded is None: + return None + + return cls.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: bytes) -> List["SubnetInfoV2"]: + """Returns a list of SubnetInfoV2 objects from a ``vec_u8``.""" + decoded = bt_decode.SubnetInfoV2.decode_vec(vec_u8) # TODO fix values + + if decoded is None: + return [] + + decoded = [cls.fix_decoded_values(d) for d in decoded] + + return decoded + custom_rpc_type_registry = { "types": { @@ -597,6 +638,35 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["owner", "AccountId"], ], }, + "DynamicPoolInfoV2": { + "type": "struct", + "type_mapping": [ + ["netuid", "u16"], + ["alpha_issuance", "u64"], + ["alpha_outstanding", "u64"], + ["alpha_reserve", "u64"], + ["tao_reserve", "u64"], + ["k", "u128"], + ], + }, + "SubnetInfoV2": { + "type": "struct", + "type_mapping": [ + ["netuid", "u16"], + ["owner", "AccountId"], + ["max_allowed_validators", "u16"], + ["scaling_law_power", "u16"], + ["subnetwork_n", "u16"], + ["max_allowed_uids", "u16"], + ["blocks_since_last_step", "Compact"], + ["network_modality", "u16"], + ["emission_values", "Compact"], + ["burn", "Compact"], + ["tao_locked", "Compact"], + ["hyperparameters", "SubnetHyperparameters"], + ["dynamic_pool", "Option"], + ], + }, "DelegateInfo": { "type": "struct", "type_mapping": [ @@ -610,6 +680,16 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["total_daily_return", "Compact"], ], }, + "DelegateInfoLight": { + "type": "struct", + "type_mapping": [ + ["delegate_ss58", "AccountId"], + ["owner_ss58", "AccountId"], + ["take", "u16"], + ["owner_stake", "Compact"], + ["total_stake", "Compact"], + ], + }, "NeuronInfo": { "type": "struct", "type_mapping": [ @@ -688,6 +768,37 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["ip_type_and_protocol", "Compact"], ], }, + "ScheduledColdkeySwapInfo": { + "type": "struct", + "type_mapping": [ + ["old_coldkey", "AccountId"], + ["new_coldkey", "AccountId"], + ["arbitration_block", "Compact"], + ], + }, + "SubnetState": { + "type": "struct", + "type_mapping": [ + ["netuid", "Compact"], + ["hotkeys", "Vec"], + ["coldkeys", "Vec"], + ["active", "Vec"], + ["validator_permit", "Vec"], + ["pruning_score", "Vec>"], + ["last_update", "Vec>"], + ["emission", "Vec>"], + ["dividends", "Vec>"], + ["incentives", "Vec>"], + ["consensus", "Vec>"], + ["trust", "Vec>"], + ["rank", "Vec>"], + ["block_at_registration", "Vec>"], + ["local_stake", "Vec>"], + ["global_stake", "Vec>"], + ["stake_weight", "Vec>"], + ["emission_history", "Vec>>"], + ], + }, "StakeInfo": { "type": "struct", "type_mapping": [ @@ -696,6 +807,31 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["stake", "Compact"], ], }, + "DynamicInfo": { + "type": "struct", + "type_mapping": [ + ["owner", "AccountId"], + ["netuid", "Compact"], + ["tempo", "Compact"], + ["last_step", "Compact"], + ["blocks_since_last_step", "Compact"], + ["emission", "Compact"], + ["alpha_in", "Compact"], + ["alpha_out", "Compact"], + ["tao_in", "Compact"], + ["total_locked", "Compact"], + ["owner_locked", "Compact"], + ], + }, + "SubstakeElements": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["netuid", "Compact"], + ["stake", "Compact"], + ], + }, "SubnetHyperparameters": { "type": "struct", "type_mapping": [ From e58e5a8fc9b7bab61ad913b0c2e200ed6903c36d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 25 Sep 2024 16:37:22 +0200 Subject: [PATCH 002/332] Ruff --- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6df30907..acd8cd90 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -17,7 +17,7 @@ class Constants: "archive": archive_entrypoint, "local": local_entrypoint, "dev": dev_entrypoint, - "rao": rao_entrypoint + "rao": rao_entrypoint, } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index b316d8e2..415fb55e 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -571,9 +571,11 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ) return result + @dataclass class SubnetInfoV2: """Dataclass for subnet info.""" + netuid: int owner_ss58: str max_allowed_validators: int @@ -776,7 +778,7 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> List["SubnetInfoV2"]: ["arbitration_block", "Compact"], ], }, - "SubnetState": { + "SubnetState": { "type": "struct", "type_mapping": [ ["netuid", "Compact"], From 683273ff21ad1c8cc9541feb6be644cf0a0b4689 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 27 Sep 2024 12:39:09 +0200 Subject: [PATCH 003/332] Data classes --- bittensor_cli/src/bittensor/chain_data.py | 437 +++++++++++++++++++++- 1 file changed, 436 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 415fb55e..61eb20e3 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -603,7 +603,7 @@ def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetInfoV2"]: return cls.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> List["SubnetInfoV2"]: + def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfoV2"]: """Returns a list of SubnetInfoV2 objects from a ``vec_u8``.""" decoded = bt_decode.SubnetInfoV2.decode_vec(vec_u8) # TODO fix values @@ -615,6 +615,441 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> List["SubnetInfoV2"]: return decoded +@dataclass +class DynamicInfo: + owner: str + netuid: int + tempo: int + last_step: int + blocks_since_last_step: int + emission: Balance + alpha_in: Balance + alpha_out: Balance + tao_in: Balance + total_locked: Balance + owner_locked: Balance + price: Balance + k: float + is_dynamic: bool + symbol: str + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: + if len(vec_u8) == 0: + return None + decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo, is_option=True) + if decoded is None: + return None + return DynamicInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DynamicInfo"]: + decoded = from_scale_encoding( + vec_u8, ChainDataType.DynamicInfo, is_vec=True, is_option=True + ) + if decoded is None: + return [] + decoded = [DynamicInfo.fix_decoded_values(d) for d in decoded] + return decoded + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": + netuid = int(decoded["netuid"]) + symbol = Balance.get_unit(netuid) + emission = Balance.from_rao(decoded["emission"]).set_unit(0) + alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) + alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) + tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) + total_locked = Balance.from_rao(decoded["total_locked"]).set_unit( + netuid + ) + owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit( + netuid + ) + price = ( + Balance.from_tao(tao_in.tao / alpha_in.tao) + if alpha_in.tao > 0 + else Balance.from_tao(1) + ) + is_dynamic = True if decoded["alpha_in"] > 0 else False + return DynamicInfo( + owner=ss58_encode(decoded["owner"], SS58_FORMAT), + netuid=netuid, + tempo=decoded["tempo"], + last_step=decoded["last_step"], + blocks_since_last_step=decoded["blocks_since_last_step"], + emission=emission, + alpha_out=alpha_out, + alpha_in=alpha_in, + tao_in=tao_in, + total_locked=total_locked, + owner_locked=owner_locked, + price=price, + k=tao_in.rao * alpha_in.rao, + is_dynamic=is_dynamic, + symbol=symbol, + ) + + def tao_to_alpha(self, tao: Balance) -> Balance: + if self.price.tao != 0: + return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) + else: + return Balance.from_tao(0) + + def alpha_to_tao(self, alpha: Balance) -> Balance: + return Balance.from_tao(alpha.tao * self.price.tao) + + def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. + Args: + tao: Amount of TAO to stake. + Returns: + Tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_tao_in = self.tao_in + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_in.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + return alpha_returned, slippage + + def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + Args: + alpha: Amount of Alpha to stake. + Returns: + Tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_alpha_in = self.alpha_in + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_in - new_tao_reserve) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + return tao_returned, slippage + + +@dataclass +class DynamicPoolInfoV2: + """Dataclass for dynamic pool info.""" + netuid: int + alpha_issuance: int + alpha_outstanding: int + alpha_reserve: int + tao_reserve: int + k: int + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicPoolInfoV2"]: + """Returns a DynamicPoolInfoV2 object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + return from_scale_encoding(vec_u8, ChainDataType.DynamicPoolInfoV2) + + +@dataclass +class DynamicPool: + is_dynamic: bool + alpha_issuance: Balance + alpha_outstanding: Balance + alpha_reserve: Balance + tao_reserve: Balance + k: int + price: Balance + netuid: int + + def __init__( + self, + is_dynamic: bool, + netuid: int, + alpha_issuance: Union[int, Balance], + alpha_outstanding: Union[int, Balance], + alpha_reserve: Union[int, Balance], + tao_reserve: Union[int, Balance], + k: int, + ): + self.is_dynamic = is_dynamic + self.netuid = netuid + self.alpha_issuance = ( + alpha_issuance + if isinstance(alpha_issuance, Balance) + else Balance.from_rao(alpha_issuance).set_unit(netuid) + ) + self.alpha_outstanding = ( + alpha_outstanding + if isinstance(alpha_outstanding, Balance) + else Balance.from_rao(alpha_outstanding).set_unit(netuid) + ) + self.alpha_reserve = ( + alpha_reserve + if isinstance(alpha_reserve, Balance) + else Balance.from_rao(alpha_reserve).set_unit(netuid) + ) + self.tao_reserve = ( + tao_reserve + if isinstance(tao_reserve, Balance) + else Balance.from_rao(tao_reserve).set_unit(0) + ) + self.k = k + if is_dynamic: + if self.alpha_reserve.tao > 0: + self.price = Balance.from_tao( + self.tao_reserve.tao / self.alpha_reserve.tao + ) + else: + self.price = Balance.from_tao(0.0) + else: + self.price = Balance.from_tao(1.0) + + def __str__(self) -> str: + return (f"DynamicPool( alpha_issuance={self.alpha_issuance}, " + f"alpha_outstanding={self.alpha_outstanding}, " + f"alpha_reserve={self.alpha_reserve}, " + f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )") + + def __repr__(self) -> str: + return self.__str__() + + def tao_to_alpha(self, tao: Balance) -> Balance: + if self.price.tao != 0: + return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) + else: + return Balance.from_tao(0) + + def alpha_to_tao(self, alpha: Balance) -> Balance: + return Balance.from_tao(alpha.tao * self.price.tao) + + def tao_to_alpha_with_slippage(self, tao: Balance) -> Tuple[Balance, Balance]: + """ + Returns an estimate of how much Alpha would a staker receive if they stake their tao + using the current pool state + Args: + tao: Amount of TAO to stake. + Returns: + Tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_tao_in = self.tao_reserve + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_reserve.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + return alpha_returned, slippage + + def alpha_to_tao_with_slippage(self, alpha: Balance) -> Tuple[Balance, Balance]: + """ + Returns an estimate of how much TAO would a staker receive if they unstake their + alpha using the current pool state + Args: + alpha: Amount of Alpha to stake. + Returns: + Tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_alpha_in = self.alpha_reserve + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_reserve - new_tao_reserve) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + return tao_returned, slippage + + +@dataclass +class ScheduledColdkeySwapInfo: + """Dataclass for scheduled coldkey swap information.""" + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + """Fixes the decoded values.""" + return cls( + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), + arbitration_block=decoded["arbitration_block"], + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["ScheduledColdkeySwapInfo"]: + """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) + if decoded is None: + return None + + return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["ScheduledColdkeySwapInfo"]: + """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + ) + if decoded is None: + return [] + + return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) + if decoded is None: + return None + return [ + ss58_encode(account_id, SS58_FORMAT) for account_id in decoded + ] + + +@dataclass +class SubnetState: + netuid: int + hotkeys: list[str] + coldkeys: list[str] + active: list[bool] + validator_permit: list[bool] + pruning_score: list[float] + last_update: list[int] + emission: list[Balance] + dividends: list[float] + incentives: list[float] + consensus: list[float] + trust: list[float] + rank: list[float] + block_at_registration: list[int] + local_stake: list[Balance] + global_stake: list[Balance] + stake_weight: list[float] + emission_history: list[list[int]] + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: + if len(vec_u8) == 0: return None + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState, is_option=True) + if decoded is None: return None + return SubnetState.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: + decoded = from_scale_encoding( vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True ) + if decoded is None:return [] + decoded = [SubnetState.fix_decoded_values(d) for d in decoded] + return decoded + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetState": + netuid = decoded["netuid"] + return SubnetState( + netuid = netuid, + hotkeys = [ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], + coldkeys = [ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], + active = decoded["active"], + validator_permit = decoded["validator_permit"], + pruning_score = [u16_normalized_float(val) for val in decoded["pruning_score"]], + last_update = decoded["last_update"], + emission = [Balance.from_rao( val ).set_unit(netuid) for val in decoded["emission"]], + dividends = [u16_normalized_float(val) for val in decoded["dividends"]], + incentives = [u16_normalized_float(val) for val in decoded["incentives"]], + consensus = [u16_normalized_float(val) for val in decoded["consensus"]], + trust = [u16_normalized_float(val) for val in decoded["trust"]], + rank = [u16_normalized_float(val) for val in decoded["rank"]], + block_at_registration = decoded["block_at_registration"], + local_stake = [Balance.from_rao( val ).set_unit(netuid) for val in decoded["local_stake"]], + global_stake = [Balance.from_rao( val ).set_unit(0) for val in decoded["global_stake"]], + stake_weight = [u16_normalized_float(val) for val in decoded["stake_weight"]], + emission_history = decoded["emission_history"] + ) + +class SubstakeElements: + @staticmethod + def decode(result: list[int]) -> list[dict]: + descaled = from_scale_encoding( + input_=result, type_name=ChainDataType.SubstakeElements, is_vec=True + ) + result = [] + for item in descaled: + result.append( + { + "hotkey": ss58_encode(item["hotkey"], SS58_FORMAT), + "coldkey": ss58_encode(item["coldkey"], SS58_FORMAT), + "netuid": item["netuid"], + "stake": Balance.from_rao(item["stake"]), + } + ) + return result + + + custom_rpc_type_registry = { "types": { "SubnetInfo": { From 545759c70ea32b2469151c00bee0e85f0751bca7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 27 Sep 2024 12:46:14 +0200 Subject: [PATCH 004/332] Balance update. --- bittensor_cli/src/bittensor/balances.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 1e678c9d..4943edd7 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -18,6 +18,7 @@ # DEALINGS IN THE SOFTWARE. from typing import Union +from bittensor_cli.src import UNITS class Balance: @@ -279,3 +280,21 @@ def from_rao(amount: int): :return: A Balance object representing the given amount. """ return Balance(amount) + + @staticmethod + def get_unit(netuid: int): + units = UNITS + base = len(units) + if netuid < base: + return units[netuid] + else: + result = "" + while netuid > 0: + result = units[netuid % base] + result + netuid //= base + return result + + def set_unit(self, netuid: int): + self.unit = Balance.get_unit(netuid) + self.rao_unit = Balance.get_unit(netuid) + return self From 58c623bfb89ba5b3f231f9a22a342d5fc69492c7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 27 Sep 2024 12:50:10 +0200 Subject: [PATCH 005/332] Ruff --- bittensor_cli/src/bittensor/chain_data.py | 83 +++++++++++++---------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 61eb20e3..b6bb985c 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -660,12 +660,8 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) - total_locked = Balance.from_rao(decoded["total_locked"]).set_unit( - netuid - ) - owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit( - netuid - ) + total_locked = Balance.from_rao(decoded["total_locked"]).set_unit(netuid) + owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit(netuid) price = ( Balance.from_tao(tao_in.tao / alpha_in.tao) if alpha_in.tao > 0 @@ -766,6 +762,7 @@ def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: @dataclass class DynamicPoolInfoV2: """Dataclass for dynamic pool info.""" + netuid: int alpha_issuance: int alpha_outstanding: int @@ -836,10 +833,12 @@ def __init__( self.price = Balance.from_tao(1.0) def __str__(self) -> str: - return (f"DynamicPool( alpha_issuance={self.alpha_issuance}, " - f"alpha_outstanding={self.alpha_outstanding}, " - f"alpha_reserve={self.alpha_reserve}, " - f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )") + return ( + f"DynamicPool( alpha_issuance={self.alpha_issuance}, " + f"alpha_outstanding={self.alpha_outstanding}, " + f"alpha_reserve={self.alpha_reserve}, " + f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )" + ) def __repr__(self) -> str: return self.__str__() @@ -922,6 +921,7 @@ def alpha_to_tao_with_slippage(self, alpha: Balance) -> Tuple[Balance, Balance]: @dataclass class ScheduledColdkeySwapInfo: """Dataclass for scheduled coldkey swap information.""" + old_coldkey: str new_coldkey: str arbitration_block: int @@ -966,9 +966,7 @@ def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: ) if decoded is None: return None - return [ - ss58_encode(account_id, SS58_FORMAT) for account_id in decoded - ] + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] @dataclass @@ -994,15 +992,20 @@ class SubnetState: @classmethod def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: - if len(vec_u8) == 0: return None + if len(vec_u8) == 0: + return None decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState, is_option=True) - if decoded is None: return None + if decoded is None: + return None return SubnetState.fix_decoded_values(decoded) @classmethod def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: - decoded = from_scale_encoding( vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True ) - if decoded is None:return [] + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True + ) + if decoded is None: + return [] decoded = [SubnetState.fix_decoded_values(d) for d in decoded] return decoded @@ -1010,26 +1013,35 @@ def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: def fix_decoded_values(cls, decoded: dict) -> "SubnetState": netuid = decoded["netuid"] return SubnetState( - netuid = netuid, - hotkeys = [ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], - coldkeys = [ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], - active = decoded["active"], - validator_permit = decoded["validator_permit"], - pruning_score = [u16_normalized_float(val) for val in decoded["pruning_score"]], - last_update = decoded["last_update"], - emission = [Balance.from_rao( val ).set_unit(netuid) for val in decoded["emission"]], - dividends = [u16_normalized_float(val) for val in decoded["dividends"]], - incentives = [u16_normalized_float(val) for val in decoded["incentives"]], - consensus = [u16_normalized_float(val) for val in decoded["consensus"]], - trust = [u16_normalized_float(val) for val in decoded["trust"]], - rank = [u16_normalized_float(val) for val in decoded["rank"]], - block_at_registration = decoded["block_at_registration"], - local_stake = [Balance.from_rao( val ).set_unit(netuid) for val in decoded["local_stake"]], - global_stake = [Balance.from_rao( val ).set_unit(0) for val in decoded["global_stake"]], - stake_weight = [u16_normalized_float(val) for val in decoded["stake_weight"]], - emission_history = decoded["emission_history"] + netuid=netuid, + hotkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], + coldkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], + active=decoded["active"], + validator_permit=decoded["validator_permit"], + pruning_score=[ + u16_normalized_float(val) for val in decoded["pruning_score"] + ], + last_update=decoded["last_update"], + emission=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["emission"] + ], + dividends=[u16_normalized_float(val) for val in decoded["dividends"]], + incentives=[u16_normalized_float(val) for val in decoded["incentives"]], + consensus=[u16_normalized_float(val) for val in decoded["consensus"]], + trust=[u16_normalized_float(val) for val in decoded["trust"]], + rank=[u16_normalized_float(val) for val in decoded["rank"]], + block_at_registration=decoded["block_at_registration"], + local_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["local_stake"] + ], + global_stake=[ + Balance.from_rao(val).set_unit(0) for val in decoded["global_stake"] + ], + stake_weight=[u16_normalized_float(val) for val in decoded["stake_weight"]], + emission_history=decoded["emission_history"], ) + class SubstakeElements: @staticmethod def decode(result: list[int]) -> list[dict]: @@ -1049,7 +1061,6 @@ def decode(result: list[int]) -> list[dict]: return result - custom_rpc_type_registry = { "types": { "SubnetInfo": { From f61a165ba2db7383753468d1d69c3afb0a611956 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 27 Sep 2024 17:37:08 +0200 Subject: [PATCH 006/332] Names --- bittensor_cli/src/bittensor/chain_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index b6bb985c..2cd4e580 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Any, Union import bt_decode import netaddr @@ -852,7 +852,7 @@ def tao_to_alpha(self, tao: Balance) -> Balance: def alpha_to_tao(self, alpha: Balance) -> Balance: return Balance.from_tao(alpha.tao * self.price.tao) - def tao_to_alpha_with_slippage(self, tao: Balance) -> Tuple[Balance, Balance]: + def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: """ Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state @@ -888,7 +888,7 @@ def tao_to_alpha_with_slippage(self, tao: Balance) -> Tuple[Balance, Balance]: slippage = Balance.from_tao(0) return alpha_returned, slippage - def alpha_to_tao_with_slippage(self, alpha: Balance) -> Tuple[Balance, Balance]: + def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: """ Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state From 330c382c2f53b6ece5a879318bfea93c30ecc014 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Oct 2024 13:48:51 +0200 Subject: [PATCH 007/332] dumpster fire --- bittensor_cli/src/bittensor/chain_data.py | 70 +++++++++ bittensor_cli/src/commands/stake/__init__.py | 146 +++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 2cd4e580..c839fe43 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -515,6 +515,76 @@ def delegated_list_from_vec_u8( results.append((delegate, Balance.from_rao(b))) return results +@dataclass +class DelegateInfoLite: + """ + Dataclass for light delegate information. + + Args: + hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. + owner_ss58 (str): Coldkey of the owner. + total_stake (int): Total stake of the delegate. + owner_stake (int): Own stake of the delegate. + take (float): Take of the delegate as a percentage. None if custom + """ + + hotkey_ss58: str # Hotkey of delegate + owner_ss58: str # Coldkey of owner + take: Optional[float] + total_stake: Balance # Total stake of the delegate + previous_total_stake: Optional[Balance] # Total stake of the delegate + owner_stake: Balance # Own stake of the delegate + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": + """Fixes the decoded values.""" + decoded_take = decoded["take"] + + if decoded_take == 65535: + fixed_take = None + else: + fixed_take = u16_normalized_float(decoded_take) + + return cls( + hotkey_ss58=ss58_encode( + decoded["delegate_ss58"], SS58_FORMAT + ), + owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + take=fixed_take, + total_stake=Balance.from_rao(decoded["total_stake"]), + owner_stake=Balance.from_rao(decoded["owner_stake"]), + previous_total_stake=None, + ) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfoLite"]: + """Returns a DelegateInfoLight object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLight) + + if decoded is None: + return None + + decoded = DelegateInfoLite.fix_decoded_values(decoded) + + return decoded + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfoLite"]: + """Returns a list of DelegateInfoLight objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.DelegateInfoLight, is_vec=True + ) + + if decoded is None: + return [] + + decoded = [DelegateInfoLite.fix_decoded_values(d) for d in decoded] + + return decoded + @dataclass class SubnetInfo: diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index e69de29b..c0c650e8 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -0,0 +1,146 @@ + +from typing import Optional, TYPE_CHECKING + +import rich.prompt +from rich.table import Table + +from bittensor_cli.src import DelegatesDetails +from bittensor_cli.src.bittensor.chain_data import DelegateInfo, DelegateInfoLite +from bittensor_cli.src.bittensor.utils import console, err_console + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def select_delegate(subtensor: "SubtensorInterface", netuid: int): + # Get a list of delegates and sort them by total stake in descending order + delegates: list[DelegateInfoLite] = ( + subtensor.get_delegates_by_netuid_light(netuid) + ) + delegates.sort(key=lambda x: x.total_stake, reverse=True) + + # Get registered delegates details. + registered_delegate_info: Optional[DelegatesDetails] = await subtensor.get_delegate_identities() + + # Create a table to display delegate information + table = Table( + show_header=True, + header_style="bold", + border_style="rgb(7,54,66)", + style="rgb(0,43,54)", + ) + + # Add columns to the table with specific styles + table.add_column("Index", style="rgb(253,246,227)", no_wrap=True) + table.add_column("Delegate Name", no_wrap=True) + table.add_column("Hotkey SS58", style="rgb(211,54,130)", no_wrap=True) + table.add_column("Owner SS58", style="rgb(133,153,0)", no_wrap=True) + table.add_column("Take", style="rgb(181,137,0)", no_wrap=True) + table.add_column( + "Total Stake", style="rgb(38,139,210)", no_wrap=True, justify="right" + ) + table.add_column( + "Owner Stake", style="rgb(220,50,47)", no_wrap=True, justify="right" + ) + # table.add_column("Return per 1000", style="rgb(108,113,196)", no_wrap=True, justify="right") + # table.add_column("Total Daily Return", style="rgb(42,161,152)", no_wrap=True, justify="right") + + # List to store visible delegates + visible_delegates = [] + + # TODO: Add pagination to handle large number of delegates more efficiently + # Iterate through delegates and display their information + idx = 0 + selected_idx = None + while True: + if idx < len(delegates): + delegate = delegates[idx] + + # Add delegate to visible list + visible_delegates.append(delegate) + + # Add a row to the table with delegate information + table.add_row( + str(idx), + registered_delegate_info[delegate.hotkey_ss58].name + if delegate.hotkey_ss58 in registered_delegate_info + else "", + delegate.hotkey_ss58[:5] + + "..." + + delegate.hotkey_ss58[-5:], # Show truncated hotkey + delegate.owner_ss58[:5] + + "..." + + delegate.owner_ss58[-5:], # Show truncated owner address + f"{delegate.take:.6f}", + f"τ{delegate.total_stake.tao:,.4f}", + f"τ{delegate.owner_stake.tao:,.4f}", + # f"τ{delegate.return_per_1000.tao:,.4f}", + # f"τ{delegate.total_daily_return.tao:,.4f}", + ) + + # Clear console and print updated table + console.clear() + console.print(table) + + # Prompt user for input + user_input: str = rich.prompt.Prompt.ask( + 'Press Enter to scroll, enter a number (1-N) to select, or type "h" for help: ', + choices=["h"] + [str(x) for x in range(1, len(delegates) - 1)], + show_choices=True + ) + + # Add a help option to display information about each column + if user_input == "h": + console.print("\nColumn Information:") + console.print( + "[rgb(253,246,227)]Index:[/rgb(253,246,227)] Position in the list of delegates" + ) + console.print( + "[rgb(211,54,130)]Hotkey SS58:[/rgb(211,54,130)] Truncated public key of the delegate's hotkey" + ) + console.print( + "[rgb(133,153,0)]Owner SS58:[/rgb(133,153,0)] Truncated public key of the delegate's owner" + ) + console.print( + "[rgb(181,137,0)]Take:[/rgb(181,137,0)] Percentage of rewards the delegate takes" + ) + console.print( + "[rgb(38,139,210)]Total Stake:[/rgb(38,139,210)] Total amount staked to this delegate" + ) + console.print( + "[rgb(220,50,47)]Owner Stake:[/rgb(220,50,47)] Amount staked by the delegate owner" + ) + console.print( + "[rgb(108,113,196)]Return per 1000:[/rgb(108,113,196)] Estimated return for 1000 Tao staked" + ) + console.print( + "[rgb(42,161,152)]Total Daily Return:[/rgb(42,161,152)] Estimated total daily return for all stake" + ) + console.print("\nPress Enter to continue...") + input() + continue + + # If user presses Enter, continue to next delegate + elif user_input: + selected_idx = int(user_input) + break + + if idx < len(delegates): + idx += 1 + + # TODO( const ): uncomment for check + # Add a confirmation step before returning the selected delegate + # console.print(f"\nSelected delegate: [rgb(211,54,130)]{visible_delegates[selected_idx].hotkey_ss58}[/rgb(211,54,130)]") + # console.print(f"Take: [rgb(181,137,0)]{visible_delegates[selected_idx].take:.6f}[/rgb(181,137,0)]") + # console.print(f"Total Stake: [rgb(38,139,210)]{visible_delegates[selected_idx].total_stake}[/rgb(38,139,210)]") + + # confirmation = Prompt.ask("Do you want to proceed with this delegate? (y/n)") + # if confirmation.lower() != 'yes' and confirmation.lower() != 'y': + # return select_delegate( subtensor, netuid ) + + # Return the selected delegate + + if selected_idx is None: + raise IndexError("You must make a selection.") + else: + return delegates[selected_idx] From 881722df678684fef0f8a9295f5bc8a64bd03e5d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Oct 2024 14:10:23 +0200 Subject: [PATCH 008/332] Slightly better --- bittensor_cli/src/bittensor/chain_data.py | 5 +- .../src/bittensor/subtensor_interface.py | 31 +++ bittensor_cli/src/commands/stake/__init__.py | 179 +++++++++--------- 3 files changed, 127 insertions(+), 88 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index c839fe43..61e2718b 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -515,6 +515,7 @@ def delegated_list_from_vec_u8( results.append((delegate, Balance.from_rao(b))) return results + @dataclass class DelegateInfoLite: """ @@ -546,9 +547,7 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": fixed_take = u16_normalized_float(decoded_take) return cls( - hotkey_ss58=ss58_encode( - decoded["delegate_ss58"], SS58_FORMAT - ), + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), take=fixed_take, total_stake=Balance.from_rao(decoded["total_stake"]), diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 62063c20..d4ada27d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -24,6 +24,7 @@ NeuronInfo, SubnetHyperparameters, decode_account_id, + DelegateInfoLite, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance @@ -1087,3 +1088,33 @@ async def get_delegate_identities( ) return all_delegates_details + + async def get_delegates_by_netuid_light( + self, netuid: int, block_hash: Optional[str] = None + ) -> list[DelegateInfoLite]: + """ + Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the neurons that are actively involved in the network's delegation system. + + Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of trust and responsibility among participating neurons. + + Args: + netuid: the netuid to query + block_hash: The hash of the blockchain block number for the query. + + Returns: + A list of DelegateInfo objects detailing each delegate's characteristics. + + """ + + params = [netuid] if not block_hash else [netuid, block_hash] + json_body = await self.substrate.rpc_request( + method="delegateInfo_getDelegatesLight", # custom rpc method + params=params, + ) + + result = json_body["result"] + + if result in (None, []): + return [] + + return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index c0c650e8..8e28c089 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -1,4 +1,3 @@ - from typing import Optional, TYPE_CHECKING import rich.prompt @@ -15,12 +14,11 @@ async def select_delegate(subtensor: "SubtensorInterface", netuid: int): # Get a list of delegates and sort them by total stake in descending order delegates: list[DelegateInfoLite] = ( - subtensor.get_delegates_by_netuid_light(netuid) - ) - delegates.sort(key=lambda x: x.total_stake, reverse=True) + await subtensor.get_delegates_by_netuid_light(netuid) + ).sort(key=lambda x: x.total_stake, reverse=True) # Get registered delegates details. - registered_delegate_info: Optional[DelegatesDetails] = await subtensor.get_delegate_identities() + registered_delegate_info = await subtensor.get_delegate_identities() # Create a table to display delegate information table = Table( @@ -48,85 +46,90 @@ async def select_delegate(subtensor: "SubtensorInterface", netuid: int): # List to store visible delegates visible_delegates = [] - # TODO: Add pagination to handle large number of delegates more efficiently - # Iterate through delegates and display their information - idx = 0 - selected_idx = None - while True: - if idx < len(delegates): - delegate = delegates[idx] - - # Add delegate to visible list - visible_delegates.append(delegate) - - # Add a row to the table with delegate information - table.add_row( - str(idx), - registered_delegate_info[delegate.hotkey_ss58].name - if delegate.hotkey_ss58 in registered_delegate_info - else "", - delegate.hotkey_ss58[:5] - + "..." - + delegate.hotkey_ss58[-5:], # Show truncated hotkey - delegate.owner_ss58[:5] - + "..." - + delegate.owner_ss58[-5:], # Show truncated owner address - f"{delegate.take:.6f}", - f"τ{delegate.total_stake.tao:,.4f}", - f"τ{delegate.owner_stake.tao:,.4f}", - # f"τ{delegate.return_per_1000.tao:,.4f}", - # f"τ{delegate.total_daily_return.tao:,.4f}", - ) - - # Clear console and print updated table - console.clear() - console.print(table) - - # Prompt user for input - user_input: str = rich.prompt.Prompt.ask( + def get_user_input(): + return rich.prompt.Prompt.ask( 'Press Enter to scroll, enter a number (1-N) to select, or type "h" for help: ', choices=["h"] + [str(x) for x in range(1, len(delegates) - 1)], - show_choices=True + show_choices=True, ) - # Add a help option to display information about each column - if user_input == "h": - console.print("\nColumn Information:") - console.print( - "[rgb(253,246,227)]Index:[/rgb(253,246,227)] Position in the list of delegates" - ) - console.print( - "[rgb(211,54,130)]Hotkey SS58:[/rgb(211,54,130)] Truncated public key of the delegate's hotkey" - ) - console.print( - "[rgb(133,153,0)]Owner SS58:[/rgb(133,153,0)] Truncated public key of the delegate's owner" - ) - console.print( - "[rgb(181,137,0)]Take:[/rgb(181,137,0)] Percentage of rewards the delegate takes" - ) - console.print( - "[rgb(38,139,210)]Total Stake:[/rgb(38,139,210)] Total amount staked to this delegate" - ) - console.print( - "[rgb(220,50,47)]Owner Stake:[/rgb(220,50,47)] Amount staked by the delegate owner" - ) - console.print( - "[rgb(108,113,196)]Return per 1000:[/rgb(108,113,196)] Estimated return for 1000 Tao staked" - ) - console.print( - "[rgb(42,161,152)]Total Daily Return:[/rgb(42,161,152)] Estimated total daily return for all stake" - ) - console.print("\nPress Enter to continue...") - input() - continue - - # If user presses Enter, continue to next delegate - elif user_input: - selected_idx = int(user_input) - break - - if idx < len(delegates): - idx += 1 + # TODO: Add pagination to handle large number of delegates more efficiently + # Iterate through delegates and display their information + + def loop_selections(): + idx = 0 + selected_idx = None + while True: + if idx < len(delegates): + delegate = delegates[idx] + + # Add delegate to visible list + visible_delegates.append(delegate) + + # Add a row to the table with delegate information + table.add_row( + str(idx), + registered_delegate_info[delegate.hotkey_ss58].name + if delegate.hotkey_ss58 in registered_delegate_info + else "", + delegate.hotkey_ss58[:5] + + "..." + + delegate.hotkey_ss58[-5:], # Show truncated hotkey + delegate.owner_ss58[:5] + + "..." + + delegate.owner_ss58[-5:], # Show truncated owner address + f"{delegate.take:.6f}", + f"τ{delegate.total_stake.tao:,.4f}", + f"τ{delegate.owner_stake.tao:,.4f}", + # f"τ{delegate.return_per_1000.tao:,.4f}", + # f"τ{delegate.total_daily_return.tao:,.4f}", + ) + + # Clear console and print updated table + console.clear() + console.print(table) + + # Prompt user for input + user_input: str = get_user_input() + + # Add a help option to display information about each column + if user_input == "h": + console.print("\nColumn Information:") + console.print( + "[rgb(253,246,227)]Index:[/rgb(253,246,227)] Position in the list of delegates" + ) + console.print( + "[rgb(211,54,130)]Hotkey SS58:[/rgb(211,54,130)] Truncated public key of the delegate's hotkey" + ) + console.print( + "[rgb(133,153,0)]Owner SS58:[/rgb(133,153,0)] Truncated public key of the delegate's owner" + ) + console.print( + "[rgb(181,137,0)]Take:[/rgb(181,137,0)] Percentage of rewards the delegate takes" + ) + console.print( + "[rgb(38,139,210)]Total Stake:[/rgb(38,139,210)] Total amount staked to this delegate" + ) + console.print( + "[rgb(220,50,47)]Owner Stake:[/rgb(220,50,47)] Amount staked by the delegate owner" + ) + console.print( + "[rgb(108,113,196)]Return per 1000:[/rgb(108,113,196)] Estimated return for 1000 Tao staked" + ) + console.print( + "[rgb(42,161,152)]Total Daily Return:[/rgb(42,161,152)] Estimated total daily return for all stake" + ) + user_input = get_user_input() + + # If user presses Enter, continue to next delegate + if user_input and user_input != "h": + selected_idx = int(user_input) + break + + if idx < len(delegates): + idx += 1 + + return selected_idx # TODO( const ): uncomment for check # Add a confirmation step before returning the selected delegate @@ -139,8 +142,14 @@ async def select_delegate(subtensor: "SubtensorInterface", netuid: int): # return select_delegate( subtensor, netuid ) # Return the selected delegate - - if selected_idx is None: - raise IndexError("You must make a selection.") - else: - return delegates[selected_idx] + while True: + selected_idx_ = loop_selections() + if selected_idx_ is None: + if not rich.prompt.Confirm.ask( + "You must make a selection. Loop through again?" + ): + raise IndexError + else: + continue + else: + return delegates[selected_idx_] From c72ab2120cc5e66a6fd5640a315fff78510a67c7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Oct 2024 15:41:32 +0200 Subject: [PATCH 009/332] Renamed DelegateInfoLight to DelegateInfoLite, as Samuel states this is correct. --- bittensor_cli/src/bittensor/chain_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 61e2718b..713d8da0 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -557,11 +557,11 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": @classmethod def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfoLite"]: - """Returns a DelegateInfoLight object from a ``vec_u8``.""" + """Returns a DelegateInfoLite object from a ``vec_u8``.""" if len(vec_u8) == 0: return None - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLight) + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLite) if decoded is None: return None @@ -572,9 +572,9 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfoLite"]: @classmethod def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfoLite"]: - """Returns a list of DelegateInfoLight objects from a ``vec_u8``.""" + """Returns a list of DelegateInfoLite objects from a ``vec_u8``.""" decoded = from_scale_encoding( - vec_u8, ChainDataType.DelegateInfoLight, is_vec=True + vec_u8, ChainDataType.DelegateInfoLite, is_vec=True ) if decoded is None: @@ -1197,7 +1197,7 @@ def decode(result: list[int]) -> list[dict]: ["total_daily_return", "Compact"], ], }, - "DelegateInfoLight": { + "DelegateInfoLite": { "type": "struct", "type_mapping": [ ["delegate_ss58", "AccountId"], From d421ba92a5ce36e23217bcf3fd229d6949faab06 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Oct 2024 16:13:22 +0200 Subject: [PATCH 010/332] Stake Add ported mostly --- bittensor_cli/src/bittensor/chain_data.py | 2 +- bittensor_cli/src/commands/stake/__init__.py | 10 +- bittensor_cli/src/commands/stake/stake.py | 149 ++++++++++++++++++- 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 713d8da0..cd75389b 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -556,7 +556,7 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": ) @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfoLite"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfoLite"]: """Returns a DelegateInfoLite object from a ``vec_u8``.""" if len(vec_u8) == 0: return None diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index 8e28c089..6b0b0fc9 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -46,20 +46,20 @@ async def select_delegate(subtensor: "SubtensorInterface", netuid: int): # List to store visible delegates visible_delegates = [] - def get_user_input(): + def get_user_input() -> str: return rich.prompt.Prompt.ask( 'Press Enter to scroll, enter a number (1-N) to select, or type "h" for help: ', - choices=["h"] + [str(x) for x in range(1, len(delegates) - 1)], + choices=["", "h"] + [str(x) for x in range(1, len(delegates) - 1)], show_choices=True, ) # TODO: Add pagination to handle large number of delegates more efficiently # Iterate through delegates and display their information - def loop_selections(): + def loop_selections() -> Optional[int]: idx = 0 selected_idx = None - while True: + while idx < len(delegates): if idx < len(delegates): delegate = delegates[idx] @@ -146,7 +146,7 @@ def loop_selections(): selected_idx_ = loop_selections() if selected_idx_ is None: if not rich.prompt.Confirm.ask( - "You must make a selection. Loop through again?" + "You've reached the end of the list. You must make a selection. Loop through again?" ): raise IndexError else: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 819f02cc..e59eed6a 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -8,7 +8,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm +from rich.prompt import Confirm, FloatPrompt from rich.table import Table, Column import typer @@ -1147,6 +1147,153 @@ async def get_all_wallet_accounts( ) +async def stake_add_new( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: Optional[int], + stake_all: bool, + amount: float, + staking_address_ss58: str, + delegate: bool, + prompt: bool, +): + """ + + Args: + wallet: wallet object + subtensor: SubtensorInterface object + netuid: the netuid to stake to (None indicates all subnets) + stake_all: whether to stake all available balance + amount: specified amount of balance to stake + staking_address_ss58: the hotkey to use for the stake + delegate: whether to delegate stake + prompt: whether to prompt the user + + Returns: + + """ + netuids = ( + [netuid] if netuid is not None else await subtensor.get_all_subnet_netuids() + ) + # Init the table. + table = Table( + title=f"[white]Staking operation from Coldkey SS58[/white]: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", + width=console.width - 5, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + + # Determine the amount we are staking. + rows = [] + stake_amount_balance = [] + current_stake_balances = [] + current_wallet_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + current_wallet_balance = current_wallet_balance_[ + wallet.coldkeypub.ss58_address + ].set_unit(0) + remaining_wallet_balance = current_wallet_balance + max_slippage = 0 + + for netuid in netuids: + # Check that the subnet exists. + # TODO gather this + dynamic_info = subtensor.get_subnet_dynamic_info(netuid) # TODO add + if not dynamic_info: + err_console.print(f"Subnet with netuid: {netuid} does not exist.") + continue + + # Get old staking balance. + # TODO gather this + current_stake_balance: Balance = ( + subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( # TODO add/await + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid, + ).set_unit(netuid) + ) + current_stake_balances.append(current_stake_balance) + + # Get the amount. + amount_to_stake_as_balance = None + if amount: + amount_to_stake_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_stake_as_balance = current_wallet_balance / len(netuids) + elif ( + not amount and not max_stake + ): # TODO should this be an Option? Asked in Cortex channel https://discord.com/channels/799672011265015819/1176889593136693339/1291756735316365456 + if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): + amount_to_stake_as_balance = remaining_wallet_balance + else: + try: + amount = FloatPrompt.ask( + f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}" + ) + amount_to_stake_as_balance = Balance.from_tao(amount) + except ValueError: + err_console.print( + f":cross_mark:[red]Invalid amount: {amount}[/red]" + ) + return False + stake_amount_balance.append(amount_to_stake_as_balance) + + # Check enough to stake. + amount_to_stake_as_balance.set_unit(0) + if amount_to_stake_as_balance > remaining_wallet_balance: + err_console.print( + f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " + f"staking amount: {amount_to_stake_as_balance}[/bold white]" + ) + return False + remaining_wallet_balance -= amount_to_stake_as_balance + + # Slippage warning + received_amount, slippage = dynamic_info.tao_to_alpha_with_slippage( + amount_to_stake_as_balance + ) + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "N/A" + max_slippage = max(slippage_pct_float, max_slippage) + rows.append( + ( + str(netuid), + # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", + f"{staking_address_ss58}", + str(amount_to_stake_as_balance), + str(1 / float(dynamic_info.price)) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + str(received_amount.set_unit(netuid)), + str(slippage_pct), + ) + ) + + async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", From 722836d6d179931c25c1eac89f62bf3aeded9ff1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Oct 2024 17:28:04 +0200 Subject: [PATCH 011/332] Stake Add additions --- bittensor_cli/src/commands/stake/stake.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index e59eed6a..0d26c7ea 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1177,7 +1177,8 @@ async def stake_add_new( ) # Init the table. table = Table( - title=f"[white]Staking operation from Coldkey SS58[/white]: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", + title="[white]Staking operation from Coldkey SS58[/white]: " + f"[bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", width=console.width - 5, safe_box=True, padding=(0, 1), From 5930f64b1fea852544d2c100eab845ba785e2258 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Oct 2024 16:43:53 +0200 Subject: [PATCH 012/332] New set children --- .../src/commands/stake/children_hotkeys.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 07472240..c47c71d1 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -270,6 +270,7 @@ def prepare_child_proportions(children_with_proportions): async def get_children( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int] = None ): + # TODO rao asks separately for the hotkey from the user, should we do this, or the way we do it now? """ Retrieves the child hotkeys for the specified wallet. @@ -486,6 +487,79 @@ async def _render_table( return children +async def set_children_new( + wallet: Wallet, + subtensor: "SubtensorInterface", + children: list[str], + proportions: list[float], + hotkey: str, + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + prompt: bool = True +): + """Set children hotkeys.""" + # Validate children SS58 addresses + for child in children: + if not is_valid_ss58_address(child): + err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") + return + if child == wallet.hotkey.ss58_address: + err_console.print(":cross_mark:[red] Cannot set yourself as a child.[/red]") + return + + total_proposed = sum(proportions) + if total_proposed > 1: + raise ValueError( + f"Invalid proportion: The sum of all proportions cannot be greater than 1. " + f"Proposed sum of proportions is {total_proposed}." + ) + children_with_proportions = list(zip(proportions, children)) + if netuid: + success, message = await set_children_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + hotkey=hotkey, + children_with_proportions=children_with_proportions, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # Result + if success: + if wait_for_inclusion and wait_for_finalization: + console.print("New Status:") + await get_children(wallet, subtensor, netuid) + console.print( + ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" + ) + else: + console.print( + f":cross_mark:[red] Unable to set children hotkeys.[/red] {message}" + ) + else: + # set children on all subnets that parent is registered on + netuids = await subtensor.get_all_subnet_netuids() + for netuid in netuids: + if netuid == 0: # dont include root network + continue + console.print(f"Setting children on netuid {netuid}.") + await set_children_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + hotkey=hotkey, + children_with_proportions=children_with_proportions, + prompt=prompt, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + console.print( + ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" + ) + + async def set_children( wallet: Wallet, subtensor: "SubtensorInterface", @@ -565,6 +639,7 @@ async def revoke_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ): + # TODO seek clarification on use of asking hotkey vs how we do it now """ Revokes the children hotkeys associated with a given network identifier (netuid). """ From 253fc66df399e97a4ed99b668c86c09913c8ff74 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Oct 2024 17:50:24 +0200 Subject: [PATCH 013/332] Stake list initial --- bittensor_cli/src/commands/stake/stake.py | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 0d26c7ea..43ab5621 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -14,6 +14,7 @@ from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( console, create_table, @@ -1604,3 +1605,31 @@ async def unstake( wait_for_inclusion=True, prompt=prompt, ) + + +async def stake_list( + wallet: Wallet, + subtensor: "SubtensorInterface" +): + substakes = subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address] + )[wallet.coldkeypub.ss58_address] + + # Get registered delegates details. + registered_delegate_info = await subtensor.get_delegate_identities() + + # Token pricing info. + dynamic_info = await subtensor.get_all_subnet_dynamic_info() + emission_drain_tempo = int(await subtensor.substrate.query("SubtensorModule", "HotkeyEmissionTempo")) + balance = (await subtensor.get_balance(wallet.coldkeypub.ss58_address))[wallet.coldkeypub.ss58_address] + + # Iterate over substakes and aggregate them by hotkey. + hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + + for substake in substakes: + hotkey = substake.hotkey_ss58 + if substake.stake.rao == 0: + continue + if hotkey not in hotkeys_to_substakes: + hotkeys_to_substakes[hotkey] = [] + hotkeys_to_substakes[hotkey].append(substake) From 028483070616d97a8ce40c76fea71574ef49d0f0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Oct 2024 21:55:02 +0200 Subject: [PATCH 014/332] Progress --- .../src/bittensor/subtensor_interface.py | 24 ++++ .../src/commands/stake/children_hotkeys.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 128 ++++++++++++++++-- 3 files changed, 140 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d4ada27d..fdabba62 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -25,6 +25,7 @@ SubnetHyperparameters, decode_account_id, DelegateInfoLite, + DynamicInfo, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance @@ -1118,3 +1119,26 @@ async def get_delegates_by_netuid_light( return [] return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet + + async def get_subnet_dynamic_info(self, netuid: int) -> Optional["DynamicInfo"]: + json = await self.substrate.rpc_request( + method="subnetInfo_getDynamicInfo", params=[netuid, None] + ) + subnets = DynamicInfo.from_vec_u8(json["result"]) + return subnets + + async def get_stake_for_coldkey_and_hotkey_on_netuid( + self, + hotkey_ss58: str, + coldkey_ss58: str, + netuid: int, + block_hash: Optional[str] = None, + ) -> Optional["Balance"]: + """Returns the stake under a coldkey - hotkey - netuid pairing""" + _result = await self.substrate.query( + "SubtensorModule", "Alpha", [hotkey_ss58, coldkey_ss58, netuid], block_hash + ) + if _result is None: + return None + else: + return Balance.from_rao(_result).set_unit(int(netuid)) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index c47c71d1..c472bcb1 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -496,7 +496,7 @@ async def set_children_new( netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, - prompt: bool = True + prompt: bool = True, ): """Set children hotkeys.""" # Validate children SS58 addresses diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 43ab5621..ee1cf0cc 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1157,6 +1157,10 @@ async def stake_add_new( staking_address_ss58: str, delegate: bool, prompt: bool, + max_stake: float, + include_hotkeys: list[str], + exclude_hotkeys: list[str], + hotkey_ss58: Optional[str] = None, ): """ @@ -1179,7 +1183,7 @@ async def stake_add_new( # Init the table. table = Table( title="[white]Staking operation from Coldkey SS58[/white]: " - f"[bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", + f"[bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", width=console.width - 5, safe_box=True, padding=(0, 1), @@ -1214,10 +1218,12 @@ async def stake_add_new( remaining_wallet_balance = current_wallet_balance max_slippage = 0 - for netuid in netuids: + all_dynamic_info = await asyncio.gather( + *[subtensor.get_subnet_dynamic_info(x) for x in netuids] + ) + + for netuid, dynamic_info in zip(netuids, all_dynamic_info): # Check that the subnet exists. - # TODO gather this - dynamic_info = subtensor.get_subnet_dynamic_info(netuid) # TODO add if not dynamic_info: err_console.print(f"Subnet with netuid: {netuid} does not exist.") continue @@ -1239,9 +1245,7 @@ async def stake_add_new( amount_to_stake_as_balance = Balance.from_tao(amount) elif stake_all: amount_to_stake_as_balance = current_wallet_balance / len(netuids) - elif ( - not amount and not max_stake - ): # TODO should this be an Option? Asked in Cortex channel https://discord.com/channels/799672011265015819/1176889593136693339/1291756735316365456 + elif not amount and not max_stake: if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): amount_to_stake_as_balance = remaining_wallet_balance else: @@ -1294,6 +1298,103 @@ async def stake_add_new( str(slippage_pct), ) ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column("Hotkey", justify="center", style="light_salmon3") + table.add_column( + f"Amount ({Balance.get_unit(0)})", justify="center", style="dark_sea_green" + ) + table.add_column( + f"Rate ({Balance.get_unit(netuid)}/{Balance.get_unit(0)})", + justify="center", + style="light_goldenrod2", + ) + table.add_column( + f"Recieved ({Balance.get_unit(netuid)})", + justify="center", + style="light_slate_blue", + ) + table.add_column("Slippage", justify="center", style="rgb(220,50,47)") + for row in rows: + table.add_row(*row) + console.print(table) + message = "" + if max_slippage > 5: + message += "-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold][yellow]WARNING:[/yellow]\tThe slippage on one of your operations is high: [bold red]{max_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. + - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. + - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + if prompt: + if not Confirm.ask("Would you like to continue?"): + return False + + async def send_extrinsic(netuid_i, amount, current): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_staked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: # TODO verbose? + console.print( + f":white_heavy_check_mark: [green]Submitted {amount} to {netuid_i}[/green]" + ) + else: + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error: {response.error_message}" + ) + else: + new_balance_, new_stake_ = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid_i, + ), + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = new_stake_.set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + console.print( + f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + + # Perform staking operation. + wallet.unlock_coldkey() + with console.status(f"\n:satellite: Staking on netuid: {netuids} ..."): + await asyncio.gather( + *[ + send_extrinsic(ni, am, curr) + for (ni, am, curr) in zip( + netuids, stake_amount_balance, current_stake_balances + ) + ] + ) async def stake_add( @@ -1607,10 +1708,7 @@ async def unstake( ) -async def stake_list( - wallet: Wallet, - subtensor: "SubtensorInterface" -): +async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): substakes = subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address] )[wallet.coldkeypub.ss58_address] @@ -1620,8 +1718,12 @@ async def stake_list( # Token pricing info. dynamic_info = await subtensor.get_all_subnet_dynamic_info() - emission_drain_tempo = int(await subtensor.substrate.query("SubtensorModule", "HotkeyEmissionTempo")) - balance = (await subtensor.get_balance(wallet.coldkeypub.ss58_address))[wallet.coldkeypub.ss58_address] + emission_drain_tempo = int( + await subtensor.substrate.query("SubtensorModule", "HotkeyEmissionTempo") + ) + balance = (await subtensor.get_balance(wallet.coldkeypub.ss58_address))[ + wallet.coldkeypub.ss58_address + ] # Iterate over substakes and aggregate them by hotkey. hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} From 6f013d2f952fdd3695b54e6dfb52964eb4721035 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Oct 2024 22:20:56 +0200 Subject: [PATCH 015/332] Progress --- .../src/bittensor/subtensor_interface.py | 4 +-- bittensor_cli/src/commands/stake/stake.py | 33 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index fdabba62..65160777 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1133,12 +1133,12 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( coldkey_ss58: str, netuid: int, block_hash: Optional[str] = None, - ) -> Optional["Balance"]: + ) -> "Balance": """Returns the stake under a coldkey - hotkey - netuid pairing""" _result = await self.substrate.query( "SubtensorModule", "Alpha", [hotkey_ss58, coldkey_ss58, netuid], block_hash ) if _result is None: - return None + return Balance(0).set_unit(netuid) else: return Balance.from_rao(_result).set_unit(int(netuid)) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index ee1cf0cc..eec8e6a4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1216,31 +1216,34 @@ async def stake_add_new( wallet.coldkeypub.ss58_address ].set_unit(0) remaining_wallet_balance = current_wallet_balance - max_slippage = 0 + max_slippage = 0.0 - all_dynamic_info = await asyncio.gather( - *[subtensor.get_subnet_dynamic_info(x) for x in netuids] + all_dynamic_info, initial_stake_balances = await asyncio.gather( + asyncio.gather(*[subtensor.get_subnet_dynamic_info(x) for x in netuids]), + asyncio.gather( + *[ + subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=x, + ) + for x in netuids + ] + ), ) - for netuid, dynamic_info in zip(netuids, all_dynamic_info): + for netuid, dynamic_info, current_stake_balance in zip( + netuids, all_dynamic_info, initial_stake_balances + ): # Check that the subnet exists. if not dynamic_info: err_console.print(f"Subnet with netuid: {netuid} does not exist.") continue - # Get old staking balance. - # TODO gather this - current_stake_balance: Balance = ( - subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( # TODO add/await - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=netuid, - ).set_unit(netuid) - ) current_stake_balances.append(current_stake_balance) # Get the amount. - amount_to_stake_as_balance = None + amount_to_stake_as_balance = Balance(0) if amount: amount_to_stake_as_balance = Balance.from_tao(amount) elif stake_all: @@ -1309,7 +1312,7 @@ async def stake_add_new( style="light_goldenrod2", ) table.add_column( - f"Recieved ({Balance.get_unit(netuid)})", + f"Received ({Balance.get_unit(netuid)})", justify="center", style="light_slate_blue", ) From c2223cca64f9bd8cbea71ab7d2695919630a1e2c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Oct 2024 22:23:22 +0200 Subject: [PATCH 016/332] Name shadowing. --- bittensor_cli/src/commands/stake/stake.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index eec8e6a4..6cfc32ca 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1343,14 +1343,14 @@ async def stake_add_new( if not Confirm.ask("Would you like to continue?"): return False - async def send_extrinsic(netuid_i, amount, current): + async def send_extrinsic(netuid_i, amount_, current): call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", call_params={ "hotkey": staking_address_ss58, "netuid": netuid_i, - "amount_staked": amount.rao, + "amount_staked": amount_.rao, }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( @@ -1361,7 +1361,7 @@ async def send_extrinsic(netuid_i, amount, current): ) if not prompt: # TODO verbose? console.print( - f":white_heavy_check_mark: [green]Submitted {amount} to {netuid_i}[/green]" + f":white_heavy_check_mark: [green]Submitted {amount_} to {netuid_i}[/green]" ) else: await response.process_events() From b53137bd307c972cfa058746f48e4f6e3adadafe Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Oct 2024 23:05:25 +0200 Subject: [PATCH 017/332] Ensure balance from rao is handled correctly --- bittensor_cli/src/bittensor/balances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 4943edd7..bc935f01 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -279,7 +279,7 @@ def from_rao(amount: int): :return: A Balance object representing the given amount. """ - return Balance(amount) + return Balance(int(amount)) @staticmethod def get_unit(netuid: int): From c092c8a1f032097d0db1243d015c9a480d90e39f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 11 Oct 2024 15:09:45 +0200 Subject: [PATCH 018/332] Stake add fully ported. --- bittensor_cli/cli.py | 38 +-- .../src/bittensor/subtensor_interface.py | 58 +++++ bittensor_cli/src/commands/stake/stake.py | 239 +++++++++++------- 3 files changed, 224 insertions(+), 111 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b012c989..00842f77 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3225,14 +3225,11 @@ def stake_add( "-m", help="Stake is sent to a hotkey only until the hotkey's total stake is less than or equal to this maximum staked TAO. If a hotkey already has stake greater than this amount, then stake is not added to this hotkey.", ), - hotkey_ss58_address: str = typer.Option( - "", - help="The ss58 address of the hotkey to stake to.", - ), include_hotkeys: str = typer.Option( "", "--include-hotkeys", "-in", + "--hotkey-ss58-address", help="Specifies hotkeys by name or ss58 address to stake to. For example, `-in hk1,hk2`", ), exclude_hotkeys: str = typer.Option( @@ -3247,6 +3244,7 @@ def stake_add( help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying " "hotkeys in `--include-hotkeys`.", ), + netuid: Optional[int] = Options.netuid, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -3294,12 +3292,7 @@ def stake_add( ) raise typer.Exit() - if ( - not wallet_hotkey - and not all_hotkeys - and not include_hotkeys - and not hotkey_ss58_address - ): + if not wallet_hotkey and not all_hotkeys and not include_hotkeys: hotkey_or_ss58 = Prompt.ask( "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", ) @@ -3308,6 +3301,7 @@ def stake_add( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) + include_hotkeys = hotkey_or_ss58 else: wallet_hotkey = hotkey_or_ss58 wallet = self.wallet_ask( @@ -3317,8 +3311,9 @@ def stake_add( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) + include_hotkeys = wallet.hotkey.ss58_address - elif all_hotkeys or include_hotkeys or exclude_hotkeys or hotkey_ss58_address: + elif all_hotkeys or include_hotkeys or exclude_hotkeys: wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) @@ -3332,33 +3327,38 @@ def stake_add( ) if include_hotkeys: - include_hotkeys = parse_to_list( + included_hotkeys = parse_to_list( include_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + included_hotkeys = [] if exclude_hotkeys: - exclude_hotkeys = parse_to_list( + excluded_hotkeys = parse_to_list( exclude_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--exclude-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + excluded_hotkeys = [] return self._run_command( stake.stake_add( wallet, self.initialize_chain(network), - amount, + netuid, stake_all, + amount, + False, + prompt, max_stake, - include_hotkeys, - exclude_hotkeys, all_hotkeys, - prompt, - hotkey_ss58_address, + included_hotkeys, + excluded_hotkeys, ) ) @@ -3372,7 +3372,7 @@ def stake_remove( False, "--unstake-all", "--all", - help="When set, this commmand unstakes all staked TAO from the specified hotkeys.", + help="When set, this command unstakes all staked TAO from the specified hotkeys.", ), amount: float = typer.Option( 0.0, "--amount", "-a", help="The amount of TAO to unstake." diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 65160777..dec3aa4a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1142,3 +1142,61 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( return Balance(0).set_unit(netuid) else: return Balance.from_rao(_result).set_unit(int(netuid)) + + async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( + self, + hotkey_ss58s: list[str], + coldkey_ss58: str, + netuids: list[int], + block_hash: Optional[str] = None, + ) -> dict[str, dict[str, "Balance"]]: + """ + Queries the stake for multiple hotkey - coldkey - netuid pairings. + + Args: + hotkey_ss58s: list of hotkey ss58 addresses + coldkey_ss58: a single coldkey ss58 address + netuids: list of netuids + block_hash: hash of the blockchain block, if any + + Returns: + { + hotkey_ss58_1: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + hotkey_ss58_2: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + ... + } + + """ + calls = [ + ( + await self.substrate.create_storage_key( + "SubtensorModule", + "Alpha", + [hk_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ) + ) + for hk_ss58 in hotkey_ss58s + for netuid in netuids + ] + batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for idx, item in enumerate(batch_call): + if hotkey_ss58s[idx] not in results: + results[hotkey_ss58s[idx]] = {} + for netuid in netuids: + value = ( + Balance.from_rao(item).set_unit(netuid) + if item is not None + else Balance(0).set_unit(netuid) + ) + results[hotkey_ss58s[idx]] = value + return results diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 6cfc32ca..63653efc 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -12,7 +12,6 @@ from rich.table import Table, Column import typer - from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( @@ -1148,19 +1147,18 @@ async def get_all_wallet_accounts( ) -async def stake_add_new( +async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int], stake_all: bool, amount: float, - staking_address_ss58: str, delegate: bool, prompt: bool, max_stake: float, + all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], - hotkey_ss58: Optional[str] = None, ): """ @@ -1170,9 +1168,12 @@ async def stake_add_new( netuid: the netuid to stake to (None indicates all subnets) stake_all: whether to stake all available balance amount: specified amount of balance to stake - staking_address_ss58: the hotkey to use for the stake - delegate: whether to delegate stake + delegate: whether to delegate stake, currently unused prompt: whether to prompt the user + max_stake: maximum amount to stake (used in combination with stake_all), currently unused + all_hotkeys: whether to stake all hotkeys + include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) + exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) Returns: @@ -1218,101 +1219,139 @@ async def stake_add_new( remaining_wallet_balance = current_wallet_balance max_slippage = 0.0 + hotkeys_to_stake_to: list[tuple[Optional[str], str]] = [] + if all_hotkeys: + # Stake to all hotkeys. + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + # Get the hotkeys to exclude. (d)efault to no exclusions. + # Exclude hotkeys that are specified. + hotkeys_to_stake_to = [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in exclude_hotkeys + ] # definitely wallets + + elif include_hotkeys: + print_verbose("Staking to only included hotkeys") + # Stake to specific hotkeys. + for hotkey_ss58_or_hotkey_name in include_hotkeys: + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + # If the hotkey is a valid ss58 address, we add it to the list. + hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) + else: + # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. + # We then get the hotkey from the wallet and add it to the list. + wallet_ = Wallet( + path=wallet.path, + name=wallet.name, + hotkey=hotkey_ss58_or_hotkey_name, + ) + hotkeys_to_stake_to.append( + (wallet_.hotkey_str, wallet_.hotkey.ss58_address) + ) + else: + # Only config.wallet.hotkey is specified. + # so we stake to that single hotkey. + print_verbose( + f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" + ) + assert wallet.hotkey is not None + hotkey_ss58_or_name = wallet.hotkey.ss58_address + hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] + all_dynamic_info, initial_stake_balances = await asyncio.gather( asyncio.gather(*[subtensor.get_subnet_dynamic_info(x) for x in netuids]), - asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=x, - ) - for x in netuids - ] + subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( + hotkey_ss58s=[x[1] for x in hotkeys_to_stake_to], + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuids=netuids, ), ) - - for netuid, dynamic_info, current_stake_balance in zip( - netuids, all_dynamic_info, initial_stake_balances - ): - # Check that the subnet exists. - if not dynamic_info: - err_console.print(f"Subnet with netuid: {netuid} does not exist.") - continue - - current_stake_balances.append(current_stake_balance) - - # Get the amount. - amount_to_stake_as_balance = Balance(0) - if amount: - amount_to_stake_as_balance = Balance.from_tao(amount) - elif stake_all: - amount_to_stake_as_balance = current_wallet_balance / len(netuids) - elif not amount and not max_stake: - if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): - amount_to_stake_as_balance = remaining_wallet_balance - else: - try: - amount = FloatPrompt.ask( - f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}" - ) - amount_to_stake_as_balance = Balance.from_tao(amount) - except ValueError: - err_console.print( - f":cross_mark:[red]Invalid amount: {amount}[/red]" - ) - return False - stake_amount_balance.append(amount_to_stake_as_balance) - - # Check enough to stake. - amount_to_stake_as_balance.set_unit(0) - if amount_to_stake_as_balance > remaining_wallet_balance: - err_console.print( - f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " - f"staking amount: {amount_to_stake_as_balance}[/bold white]" + for hk_name, hk_ss58 in hotkeys_to_stake_to: + if not is_valid_ss58_address(hk_ss58): + print_error( + f"The entered hotkey ss58 address is incorrect: {hk_name} | {hk_ss58}" ) return False - remaining_wallet_balance -= amount_to_stake_as_balance + for hotkey in hotkeys_to_stake_to: + for netuid, dynamic_info in zip(netuids, all_dynamic_info): + # Check that the subnet exists. + if not dynamic_info: + err_console.print(f"Subnet with netuid: {netuid} does not exist.") + continue + current_stake_balances.append(initial_stake_balances[hotkey[1]][netuid]) + + # Get the amount. + amount_to_stake_as_balance = Balance(0) + if amount: + amount_to_stake_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_stake_as_balance = current_wallet_balance / len(netuids) + elif not amount and not max_stake: + if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): + amount_to_stake_as_balance = remaining_wallet_balance + else: + try: + amount = FloatPrompt.ask( + f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}" + ) + amount_to_stake_as_balance = Balance.from_tao(amount) + except ValueError: + err_console.print( + f":cross_mark:[red]Invalid amount: {amount}[/red]" + ) + return False + stake_amount_balance.append(amount_to_stake_as_balance) + + # Check enough to stake. + amount_to_stake_as_balance.set_unit(0) + if amount_to_stake_as_balance > remaining_wallet_balance: + err_console.print( + f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " + f"staking amount: {amount_to_stake_as_balance}[/bold white]" + ) + return False + remaining_wallet_balance -= amount_to_stake_as_balance - # Slippage warning - received_amount, slippage = dynamic_info.tao_to_alpha_with_slippage( - amount_to_stake_as_balance - ) - if dynamic_info.is_dynamic: - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + received_amount) - if slippage + received_amount != 0 - else 0 + # Slippage warning + received_amount, slippage = dynamic_info.tao_to_alpha_with_slippage( + amount_to_stake_as_balance ) - slippage_pct = f"{slippage_pct_float:.4f} %" - else: - slippage_pct_float = 0 - slippage_pct = "N/A" - max_slippage = max(slippage_pct_float, max_slippage) - rows.append( - ( - str(netuid), - # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", - f"{staking_address_ss58}", - str(amount_to_stake_as_balance), - str(1 / float(dynamic_info.price)) - + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - str(received_amount.set_unit(netuid)), - str(slippage_pct), + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "N/A" + max_slippage = max(slippage_pct_float, max_slippage) + rows.append( + ( + str(netuid), + # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", + f"{hotkey}", + str(amount_to_stake_as_balance), + str(1 / float(dynamic_info.price)) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + str(received_amount.set_unit(netuid)), + str(slippage_pct), + ) ) - ) table.add_column("Netuid", justify="center", style="grey89") table.add_column("Hotkey", justify="center", style="light_salmon3") table.add_column( f"Amount ({Balance.get_unit(0)})", justify="center", style="dark_sea_green" ) table.add_column( - f"Rate ({Balance.get_unit(netuid)}/{Balance.get_unit(0)})", + f"Rate (per {Balance.get_unit(0)})", justify="center", style="light_goldenrod2", ) table.add_column( - f"Received ({Balance.get_unit(netuid)})", + "Received", justify="center", style="light_slate_blue", ) @@ -1389,18 +1428,34 @@ async def send_extrinsic(netuid_i, amount_, current): # Perform staking operation. wallet.unlock_coldkey() - with console.status(f"\n:satellite: Staking on netuid: {netuids} ..."): - await asyncio.gather( - *[ - send_extrinsic(ni, am, curr) - for (ni, am, curr) in zip( - netuids, stake_amount_balance, current_stake_balances - ) - ] - ) + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): + extrinsics_coroutines = [ + send_extrinsic(ni, am, curr) + for (ni, am, curr) in zip( + netuids, stake_amount_balance, current_stake_balances + ) + ] + if len(extrinsics_coroutines) == 1: + await asyncio.gather(*extrinsics_coroutines) + else: + tx_rate_limit_blocks = await subtensor.substrate.query( + module="SubtensorModule", storage_function="TxRateLimit" + ) + if tx_rate_limit_blocks > 0: + for item in extrinsics_coroutines: + await item + with console.status( + f":hourglass: [yellow]Waiting for tx rate limit:" + f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" + ): + await asyncio.sleep( + tx_rate_limit_blocks * 12 + ) # 12 sec per block + else: + await asyncio.gather(*extrinsics_coroutines) -async def stake_add( +async def stake_add_old( wallet: Wallet, subtensor: "SubtensorInterface", amount: float, From f7fc94aa07d44795dd66368c6385607817ba0132 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 11 Oct 2024 15:10:21 +0200 Subject: [PATCH 019/332] Old stake add removed. --- bittensor_cli/src/commands/stake/stake.py | 173 ---------------------- 1 file changed, 173 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 63653efc..1f94b84a 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1455,179 +1455,6 @@ async def send_extrinsic(netuid_i, amount_, current): await asyncio.gather(*extrinsics_coroutines) -async def stake_add_old( - wallet: Wallet, - subtensor: "SubtensorInterface", - amount: float, - stake_all: bool, - max_stake: float, - include_hotkeys: list[str], - exclude_hotkeys: list[str], - all_hotkeys: bool, - prompt: bool, - hotkey_ss58: Optional[str] = None, -) -> None: - """Stake token of amount to hotkey(s).""" - - async def is_hotkey_registered_any(hk: str, bh: str) -> bool: - return len(await subtensor.get_netuids_for_hotkey(hk, bh)) > 0 - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_stake_to: list[tuple[Optional[str], str]] = [] - if hotkey_ss58: - if not is_valid_ss58_address(hotkey_ss58): - print_error("The entered ss58 address is incorrect") - typer.Exit() - - # Stake to specific hotkey. - hotkeys_to_stake_to = [(None, hotkey_ss58)] - elif all_hotkeys: - # Stake to all hotkeys. - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - # Get the hotkeys to exclude. (d)efault to no exclusions. - # Exclude hotkeys that are specified. - hotkeys_to_stake_to = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in exclude_hotkeys - ] # definitely wallets - - elif include_hotkeys: - print_verbose("Staking to only included hotkeys") - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - path=wallet.path, - name=wallet.name, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys_to_stake_to.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - else: - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - print_verbose( - f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" - ) - assert wallet.hotkey is not None - hotkey_ss58_or_name = wallet.hotkey.ss58_address - hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] - - try: - # Get coldkey balance - print_verbose("Fetching coldkey balance") - wallet_balance_: dict[str, Balance] = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - block_hash = subtensor.substrate.last_block_hash - wallet_balance: Balance = wallet_balance_[wallet.coldkeypub.ss58_address] - old_balance = copy.copy(wallet_balance) - final_hotkeys: list[tuple[Optional[str], str]] = [] - final_amounts: list[Union[float, Balance]] = [] - hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - - print_verbose("Checking if hotkeys are registered") - registered_ = asyncio.gather( - *[is_hotkey_registered_any(h[1], block_hash) for h in hotkeys_to_stake_to] - ) - if max_stake: - hotkey_stakes_ = asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=h[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for h in hotkeys_to_stake_to - ] - ) - else: - - async def null(): - return [None] * len(hotkeys_to_stake_to) - - hotkey_stakes_ = null() - registered: list[bool] - hotkey_stakes: list[Optional[Balance]] - registered, hotkey_stakes = await asyncio.gather(registered_, hotkey_stakes_) - - for hotkey, reg, hotkey_stake in zip( - hotkeys_to_stake_to, registered, hotkey_stakes - ): - if not reg: - # Hotkey is not registered. - if len(hotkeys_to_stake_to) == 1: - # Only one hotkey, error - err_console.print( - f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" - ) - raise ValueError - else: - # Otherwise, print warning and skip - console.print( - f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" - ) - continue - - stake_amount_tao: float = amount - if max_stake: - stake_amount_tao = max_stake - hotkey_stake.tao - - # If the max_stake is greater than the current wallet balance, stake the entire balance. - stake_amount_tao = min(stake_amount_tao, wallet_balance.tao) - if ( - stake_amount_tao <= 0.00001 - ): # Threshold because of fees, might create a loop otherwise - # Skip hotkey if max_stake is less than current stake. - continue - wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) - - if wallet_balance.tao < 0: - # No more balance to stake. - break - - final_amounts.append(stake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. - - if len(final_hotkeys) == 0: - # No hotkeys to stake to. - err_console.print( - "Not enough balance to stake to any hotkeys or max_stake is less than current stake." - ) - raise ValueError - - if len(final_hotkeys) == 1: - # do regular stake - await add_stake_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58=final_hotkeys[0][1], - amount=None if stake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=prompt, - ) - else: - await add_stake_multiple_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if stake_all else final_amounts, - wait_for_inclusion=True, - prompt=prompt, - ) - except ValueError: - pass - - async def unstake( wallet: Wallet, subtensor: "SubtensorInterface", From db3e8bb4b4ef102862002a18c4c9807c4b16d64d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 11 Oct 2024 15:30:07 +0200 Subject: [PATCH 020/332] Updated multi fetch method. --- .../src/bittensor/subtensor_interface.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dec3aa4a..bf54946a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1149,7 +1149,7 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( coldkey_ss58: str, netuids: list[int], block_hash: Optional[str] = None, - ) -> dict[str, dict[str, "Balance"]]: + ) -> dict[str, dict[int, "Balance"]]: """ Queries the stake for multiple hotkey - coldkey - netuid pairings. @@ -1188,15 +1188,16 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( for netuid in netuids ] batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) - results = {} + results: dict[str, dict[int, "Balance"]] = {hk_ss58: {} for hk_ss58 in hotkey_ss58s} for idx, item in enumerate(batch_call): - if hotkey_ss58s[idx] not in results: - results[hotkey_ss58s[idx]] = {} - for netuid in netuids: - value = ( - Balance.from_rao(item).set_unit(netuid) - if item is not None - else Balance(0).set_unit(netuid) - ) - results[hotkey_ss58s[idx]] = value + hotkey_idx = idx // len(netuids) + netuid_idx = idx % len(netuids) + hotkey_ss58 = hotkey_ss58s[hotkey_idx] + netuid = netuids[netuid_idx] + value = ( + Balance.from_rao(item).set_unit(netuid) + if item is not None + else Balance(0).set_unit(netuid) + ) + results[hotkey_ss58][netuid] = value return results From 82d013ed1b782204705eb3c482e08329ecafcfbf Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 11 Oct 2024 15:37:47 +0200 Subject: [PATCH 021/332] Added block_hash specification --- bittensor_cli/src/bittensor/subtensor_interface.py | 10 +++++++--- bittensor_cli/src/commands/stake/stake.py | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bf54946a..5d6f55af 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1120,9 +1120,11 @@ async def get_delegates_by_netuid_light( return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet - async def get_subnet_dynamic_info(self, netuid: int) -> Optional["DynamicInfo"]: + async def get_subnet_dynamic_info( + self, netuid: int, block_hash: Optional[str] = None + ) -> Optional["DynamicInfo"]: json = await self.substrate.rpc_request( - method="subnetInfo_getDynamicInfo", params=[netuid, None] + method="subnetInfo_getDynamicInfo", params=[netuid, block_hash] ) subnets = DynamicInfo.from_vec_u8(json["result"]) return subnets @@ -1188,7 +1190,9 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( for netuid in netuids ] batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) - results: dict[str, dict[int, "Balance"]] = {hk_ss58: {} for hk_ss58 in hotkey_ss58s} + results: dict[str, dict[int, "Balance"]] = { + hk_ss58: {} for hk_ss58 in hotkey_ss58s + } for idx, item in enumerate(batch_call): hotkey_idx = idx // len(netuids) netuid_idx = idx % len(netuids) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 1f94b84a..57ad26ea 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1259,12 +1259,19 @@ async def stake_add( hotkey_ss58_or_name = wallet.hotkey.ss58_address hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] + starting_chain_head = await subtensor.substrate.get_chain_head() all_dynamic_info, initial_stake_balances = await asyncio.gather( - asyncio.gather(*[subtensor.get_subnet_dynamic_info(x) for x in netuids]), + asyncio.gather( + *[ + subtensor.get_subnet_dynamic_info(x, starting_chain_head) + for x in netuids + ] + ), subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( hotkey_ss58s=[x[1] for x in hotkeys_to_stake_to], coldkey_ss58=wallet.coldkeypub.ss58_address, netuids=netuids, + block_hash=starting_chain_head, ), ) for hk_name, hk_ss58 in hotkeys_to_stake_to: From f3581acdee507f9443f505a437642e1e201686b0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 15 Oct 2024 16:54:18 +0200 Subject: [PATCH 022/332] Check-in --- .../src/bittensor/subtensor_interface.py | 38 +++++ bittensor_cli/src/commands/stake/stake.py | 138 +++++++++++++++++- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 5d6f55af..9b93478a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1205,3 +1205,41 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( ) results[hotkey_ss58][netuid] = value return results + + async def get_stake_info_for_coldkeys( + self, coldkey_ss58_list: list[str], block_hash: Optional[str] = None + ) -> Optional[dict[str, list[StakeInfo]]]: + """ + Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple + accounts, providing a collective view of their stakes and delegations. + + Args: + coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys. + block_hash: The blockchain block number for the query. + + Returns: + A dictionary mapping each coldkey to a list of its StakeInfo objects. + + This function is useful for analyzing the stake distribution and delegation patterns of multiple + accounts simultaneously, offering a broader perspective on network participation and investment strategies. + """ + encoded_coldkeys = [ + ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list + ] + + hex_bytes_result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkeys", + params=encoded_coldkeys, + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return None + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 57ad26ea..0966ba78 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1601,9 +1601,9 @@ async def unstake( async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): - substakes = subtensor.get_stake_info_for_coldkeys( + sub_stakes = (await subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address] - )[wallet.coldkeypub.ss58_address] + ))[wallet.coldkeypub.ss58_address] # Get registered delegates details. registered_delegate_info = await subtensor.get_delegate_identities() @@ -1620,10 +1620,142 @@ async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): # Iterate over substakes and aggregate them by hotkey. hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} - for substake in substakes: + for substake in sub_stakes: hotkey = substake.hotkey_ss58 if substake.stake.rao == 0: continue if hotkey not in hotkeys_to_substakes: hotkeys_to_substakes[hotkey] = [] hotkeys_to_substakes[hotkey].append(substake) + + def table_substakes(hotkey: str, substakes: list[StakeInfo]): + # Create table structure. + name = registered_delegate_info[ + hotkey].name + f" ({hotkey})" if hotkey in registered_delegate_info else hotkey + rows = [] + total_global_tao = Balance(0) + total_tao_value = Balance(0) + for substake in substakes: + netuid = substake.netuid + pool = dynamic_info[netuid] + symbol = f"{bittensor.Balance.get_unit(netuid)}" + price = "{:.4f}{}".format(pool.price.__float__(), + f" τ/{Balance.get_unit(netuid)}\u200E") if pool.is_dynamic else ( + f" 1.0000 τ/{symbol} ") + alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid) + locked_value = Balance.from_rao(int(substake.locked.rao)).set_unit(netuid) + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value + swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage(substake.stake) + if pool.is_dynamic: + slippage_percentage = 100 * float(slippage) / float( + slippage + swapped_tao_value) if slippage + swapped_tao_value != 0 else 0 + slippage_percentage = f"[dark_red]{slippage_percentage:.3f}%[/dark_red]" + else: + slippage_percentage = '0.000%' + tao_locked = pool.tao_in + issuance = pool.alpha_out if pool.is_dynamic else tao_locked + per_block_emission = substake.emission.tao / ((emission_drain_tempo / pool.tempo) * pool.tempo) + if alpha_value.tao > 0.00009: + if issuance.tao != 0: + alpha_ownership = "{:.4f}".format((alpha_value.tao / issuance.tao) * 100) + tao_ownership = bittensor.Balance.from_tao((alpha_value.tao / issuance.tao) * tao_locked.tao) + total_global_tao += tao_ownership + else: + alpha_ownership = "0.0000" + tao_ownership = "0.0000" + rows.append([ + str(netuid), # Number + symbol, # Symbol + # f"[medium_purple]{tao_ownership}[/medium_purple] ([light_salmon3]{ alpha_ownership }[/light_salmon3][white]%[/white])", # Tao ownership. + f"[medium_purple]{tao_ownership}[/medium_purple]", # Tao ownership. + # f"[dark_sea_green]{ alpha_value }", # Alpha value + f"{substake.stake.tao:,.4f} {symbol}", + f"{pool.price.tao:.4f} τ/{symbol}", + f"[light_slate_blue]{tao_value}[/light_slate_blue]", # Tao equiv + f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap amount. + # f"[light_salmon3]{ alpha_ownership }%[/light_salmon3]", # Ownership. + f"[bold cadet_blue]YES[/bold cadet_blue]" if substake.is_registered else f"[dark_red]NO[/dark_red]", + # Registered. + str(bittensor.Balance.from_tao(per_block_emission).set_unit( + netuid)) if substake.is_registered else "[dark_red]N/A[/dark_red]", # emission per block. + f"[light_slate_blue]{locked_value}[/light_slate_blue]", # Locked value + ]) + # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") + table = Table( + title=f"[white]hotkey:[/white] [light_salmon3]{name}[/light_salmon3]\n", + width=bittensor.__console__.width - 5, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + table.add_column("[white]Netuid", footer_style="overline white", style="grey89") + table.add_column("[white]Symbol", footer_style="white", style="light_goldenrod1", justify="right", + width=5, + no_wrap=True) + table.add_column(f"[white]TAO({Balance.unit})", style="aquamarine3", justify="right", + footer=f"{total_global_tao}") + table.add_column(f"[white]Stake({Balance.get_unit(1)})", footer_style="overline white", + style="green", justify="right") + table.add_column(f"[white]Rate({Balance.unit}/{Balance.get_unit(1)})", + footer_style="white", style="light_goldenrod2", justify="center") + table.add_column( + f"[white]Value({bittensor.Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + footer_style="overline white", style="blue", justify="right", footer=f"{total_tao_value}") + table.add_column(f"[white]Swap({Balance.get_unit(1)}) -> {Balance.unit}", + footer_style="overline white", style="white", justify="right") + # table.add_column(f"[white]Control({bittensor.Balance.get_unit(1)})", style="aquamarine3", justify="right") + table.add_column("[white]Registered", style="red", justify="right") + table.add_column(f"[white]Emission({Balance.get_unit(1)}/block)", style="aquamarine3", + justify="right") + table.add_column(f"[white]Locked({Balance.get_unit(1)})", footer_style="overline white", + style="green", justify="right") + for row in rows: + table.add_row(*row) + console.print(table) + return total_global_tao, total_tao_value + + # Iterate over each hotkey and make a table + all_hotkeys_total_global_tao = Balance(0) + all_hotkeys_total_tao_value = Balance(0) + for hotkey in hotkeys_to_substakes.keys(): + stake, value = table_substakes(hotkey, hotkeys_to_substakes[hotkey]) + all_hotkeys_total_global_tao += stake + all_hotkeys_total_tao_value += value + + console.print("\n\n") + console.print( + f"Wallet:\n Coldkey SS58: [bold dark_green]{cli.config.coldkey_address}[/bold dark_green]\n Free Balance: [aquamarine3]{balance}[/aquamarine3]\n Total TAO ({bittensor.Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n Total Value ({bittensor.Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]") + console.print( + """ +[bold white]Description[/bold white]: + Each table displays information about your coldkey's staking accounts with a hotkey. + The header of the table displays the hotkey and the footer displays the total stake and total value of all your staking accounts. + The columns of the table are as follows: + - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). + - [bold white]Symbol[/bold white]: The symbol representing the subnet stake's unit. + - [bold white]TAO[/bold white]: The hotkey's TAO balance on this subnet. This is this hotkey's proportion of total TAO staked into the subnet divided by the hotkey's share of outstanding stake. + - [bold white]Stake[/bold white]: The hotkey's stake balance in subnets staking unit. + - [bold white]Rate[/bold white]: The rate of exchange between the subnet's staking unit and the subnet's TAO. + - [bold white]Value[/bold white]: The price of the hotkey's stake in TAO computed via the exchange rate. + - [bold white]Swap[/bold white]: The amount of TAO received when unstaking all of the hotkey's stake (with slippage). + - [bold white]Registered[/bold white]: Whether the hotkey is registered on this subnet. + - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. + - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). +""" + ) \ No newline at end of file From 21d9b4451a8faf8d0d2198786006c9da836c8e05 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 16 Oct 2024 22:16:42 +0200 Subject: [PATCH 023/332] Stake List --- bittensor_cli/cli.py | 75 +-- .../bittensor/async_substrate_interface.py | 4 +- bittensor_cli/src/bittensor/chain_data.py | 70 +- .../src/bittensor/subtensor_interface.py | 7 + bittensor_cli/src/commands/stake/stake.py | 597 +++++------------- 5 files changed, 244 insertions(+), 509 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 00842f77..11ba38e0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -663,9 +663,6 @@ def __init__(self): )(self.root_nominate) # stake commands - self.stake_app.command( - "show", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] - )(self.stake_show) self.stake_app.command( "add", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_add) @@ -3130,81 +3127,23 @@ def root_nominate( root.nominate(wallet, self.initialize_chain(network), prompt) ) - def stake_show( + def stake_list( self, - all_wallets: bool = typer.Option( - False, - "--all", - "--all-wallets", - "-a", - help="When set, the command checks all the coldkey wallets of the user instead of just the specified wallet.", - ), network: Optional[list[str]] = Options.network, wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + # TODO add: all-wallets, reuse_last, html_output ): - """ - Lists all the stake accounts associated with a user's wallet. - - This command provides a comprehensive view of the stakes associated with the user's coldkeys. It shows both the user's own hotkeys and also the hotkeys of the delegates to which this user has staked. - - The command lists all the stake accounts for a specified wallet or all wallets in the user's configuration directory. It displays the coldkey, balance, hotkey details (own hotkey and delegate hotkey), stake amount, and the rate of return. - - The command shows a table with the below columns: - - - Coldkey: The coldkey associated with the wallet. - - - Balance: The balance of the coldkey. - - - Hotkey: The names of the coldkey's own hotkeys and the delegate hotkeys to which this coldkey has staked. - - - Stake: The amount of TAO staked to all the hotkeys. - - - Rate: The rate of return on the stake, shown in TAO per day. - - EXAMPLE - - [green]$[/green] btcli stake show --all - """ + """List all stake accounts for wallet.""" self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: - err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " - "Please change the config to 'False' using `btcli config set`" - ) - raise typer.Exit() - if not reuse_last: - subtensor = self.initialize_chain(network) - else: - subtensor = None - - if all_wallets: - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.PATH], - validate=WV.NONE, - ) - else: - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) return self._run_command( - stake.show( - wallet, - subtensor, - all_wallets, - reuse_last, - html_output, - not self.config.get("use_cache", True), - ) + stake.stake_list(wallet, self.initialize_chain(network)) ) def stake_add( diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 0c40830d..60ec9dce 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -1707,9 +1707,7 @@ async def rpc_request( ) result = await self._make_rpc_request(payloads, runtime=runtime) if "error" in result[payload_id][0]: - raise SubstrateRequestException( - result[payload_id][0]["error"]["message"] - ) + raise SubstrateRequestException(result[payload_id][0]["error"]["message"]) if "result" in result[payload_id][0]: return result[payload_id][0] else: diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index cd75389b..9f359e92 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -133,22 +133,68 @@ class StakeInfo: hotkey_ss58: str # Hotkey address coldkey_ss58: str # Coldkey address + netuid: int stake: Balance # Stake for the hotkey-coldkey pair + locked: Balance # Stake which is locked. + emission: Balance # Emission for the hotkey-coldkey pair + drain: int + is_registered: bool @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["StakeInfo"]: - """ - Returns a list of StakeInfo objects from a `vec_u8`. - """ - decoded = bt_decode.StakeInfo.decode_vec(vec_u8) - results = [] - for d in decoded: - hotkey = decode_account_id(d.hotkey) - coldkey = decode_account_id(d.coldkey) - stake = Balance.from_rao(d.stake) - results.append(StakeInfo(hotkey, coldkey, stake)) + def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": + """Fixes the decoded values.""" + return cls( + hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), + coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), + netuid=int(decoded["netuid"]), + stake=Balance.from_rao(decoded["stake"]).set_unit(decoded["netuid"]), + locked=Balance.from_rao(decoded["locked"]).set_unit(decoded["netuid"]), + emission=Balance.from_rao(decoded["emission"]).set_unit(decoded["netuid"]), + drain=int(decoded["drain"]), + is_registered=bool(decoded["is_registered"]), + ) - return results + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["StakeInfo"]: + """Returns a StakeInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) + if decoded is None: + return None + + return StakeInfo.fix_decoded_values(decoded) + + @classmethod + def list_of_tuple_from_vec_u8( + cls, vec_u8: list[int] + ) -> dict[str, list["StakeInfo"]]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded: Optional[list[tuple[str, list[object]]]] = ( + from_scale_encoding_using_type_string( + vec_u8, type_string="Vec<(AccountId, Vec)>" + ) + ) + + if decoded is None: + return {} + + return { + ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ + StakeInfo.fix_decoded_values(d) for d in stake_info + ] + for account_id, stake_info in decoded + } + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) + if decoded is None: + return [] + + return [StakeInfo.fix_decoded_values(d) for d in decoded] @dataclass diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9b93478a..8e76171b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1243,3 +1243,10 @@ async def get_stake_info_for_coldkeys( bytes_result = bytes.fromhex(hex_bytes_result) return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + + async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: + json = await self.substrate.rpc_request( + method="subnetInfo_getAllDynamicInfo", params=[None] + ) + subnets = DynamicInfo.list_from_vec_u8(json["result"]) + return subnets diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 0966ba78..07c9891d 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -825,328 +825,6 @@ async def unstake_multiple_extrinsic( # Commands - - -async def show( - wallet: Wallet, - subtensor: Optional["SubtensorInterface"], - all_wallets: bool, - reuse_last: bool, - html_output: bool, - no_cache: bool, -): - """Show all stake accounts.""" - - async def get_stake_accounts( - wallet_, block_hash: str - ) -> dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]: - """Get stake account details for the given wallet. - - :param wallet_: The wallet object to fetch the stake account details for. - - :return: A dictionary mapping SS58 addresses to their respective stake account details. - """ - - wallet_stake_accounts = {} - - # Get this wallet's coldkey balance. - cold_balance_, stakes_from_hk, stakes_from_d = await asyncio.gather( - subtensor.get_balance( - wallet_.coldkeypub.ss58_address, block_hash=block_hash - ), - get_stakes_from_hotkeys(wallet_, block_hash=block_hash), - get_stakes_from_delegates(wallet_), - ) - - cold_balance = cold_balance_[wallet_.coldkeypub.ss58_address] - - # Populate the stake accounts with local hotkeys data. - wallet_stake_accounts.update(stakes_from_hk) - - # Populate the stake accounts with delegations data. - wallet_stake_accounts.update(stakes_from_d) - - return { - "name": wallet_.name, - "balance": cold_balance, - "accounts": wallet_stake_accounts, - } - - async def get_stakes_from_hotkeys( - wallet_, block_hash: str - ) -> dict[str, dict[str, Union[str, Balance]]]: - """Fetch stakes from hotkeys for the provided wallet. - - :param wallet_: The wallet object to fetch the stakes for. - - :return: A dictionary of stakes related to hotkeys. - """ - - async def get_all_neurons_for_pubkey(hk): - netuids = await subtensor.get_netuids_for_hotkey(hk, block_hash=block_hash) - uid_query = await asyncio.gather( - *[ - subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hk], - block_hash=block_hash, - ) - for netuid in netuids - ] - ) - uids = [_result for _result in uid_query] - neurons = await asyncio.gather( - *[ - subtensor.neuron_for_uid(uid, net) - for (uid, net) in zip(uids, netuids) - ] - ) - return neurons - - async def get_emissions_and_stake(hk: str): - neurons, stake = await asyncio.gather( - get_all_neurons_for_pubkey(hk), - subtensor.substrate.query( - module="SubtensorModule", - storage_function="Stake", - params=[hk, wallet_.coldkeypub.ss58_address], - block_hash=block_hash, - ), - ) - emission_ = sum([n.emission for n in neurons]) if neurons else 0.0 - return emission_, Balance.from_rao(stake) if stake else Balance(0) - - hotkeys = cast(list[Wallet], get_hotkey_wallets_for_wallet(wallet_)) - stakes = {} - query = await asyncio.gather( - *[get_emissions_and_stake(hot.hotkey.ss58_address) for hot in hotkeys] - ) - for hot, (emission, hotkey_stake) in zip(hotkeys, query): - stakes[hot.hotkey.ss58_address] = { - "name": hot.hotkey_str, - "stake": hotkey_stake, - "rate": emission, - } - return stakes - - async def get_stakes_from_delegates( - wallet_, - ) -> dict[str, dict[str, Union[str, Balance]]]: - """Fetch stakes from delegates for the provided wallet. - - :param wallet_: The wallet object to fetch the stakes for. - - :return: A dictionary of stakes related to delegates. - """ - delegates = await subtensor.get_delegated( - coldkey_ss58=wallet_.coldkeypub.ss58_address, block_hash=None - ) - stakes = {} - for dele, staked in delegates: - for nom in dele.nominators: - if nom[0] == wallet_.coldkeypub.ss58_address: - delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].display - if dele.hotkey_ss58 in registered_delegate_info - else None - ) - stakes[dele.hotkey_ss58] = { - "name": delegate_name if delegate_name else dele.hotkey_ss58, - "stake": nom[1], - "rate": dele.total_daily_return.tao - * (nom[1] / dele.total_stake.tao), - } - return stakes - - async def get_all_wallet_accounts( - block_hash: str, - ) -> list[dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]]: - """Fetch stake accounts for all provided wallets using a ThreadPool. - - :param block_hash: The block hash to fetch the stake accounts for. - - :return: A list of dictionaries, each dictionary containing stake account details for each wallet. - """ - - accounts_ = await asyncio.gather( - *[get_stake_accounts(w, block_hash=block_hash) for w in wallets] - ) - return accounts_ - - if not reuse_last: - cast("SubtensorInterface", subtensor) - if all_wallets: - wallets = get_coldkey_wallets_for_path(wallet.path) - valid_wallets, invalid_wallets = validate_coldkey_presence(wallets) - wallets = valid_wallets - for invalid_wallet in invalid_wallets: - print_error(f"No coldkeypub found for wallet: ({invalid_wallet.name})") - else: - wallets = [wallet] - - with console.status( - ":satellite: Retrieving account data...", spinner="aesthetic" - ): - block_hash_ = await subtensor.substrate.get_chain_head() - registered_delegate_info = await subtensor.get_delegate_identities( - block_hash=block_hash_ - ) - accounts = await get_all_wallet_accounts(block_hash=block_hash_) - - total_stake: float = 0.0 - total_balance: float = 0.0 - total_rate: float = 0.0 - rows = [] - db_rows = [] - for acc in accounts: - cast(str, acc["name"]) - cast(Balance, acc["balance"]) - rows.append([acc["name"], str(acc["balance"]), "", "", ""]) - db_rows.append( - [acc["name"], float(acc["balance"]), None, None, None, None, 0] - ) - total_balance += cast(Balance, acc["balance"]).tao - for key, value in cast(dict, acc["accounts"]).items(): - if value["name"] and value["name"] != key: - account_display_name = f"{value['name']}" - else: - account_display_name = "(~)" - rows.append( - [ - "", - "", - account_display_name, - key, - str(value["stake"]), - str(value["rate"]), - ] - ) - db_rows.append( - [ - acc["name"], - None, - value["name"], - float(value["stake"]), - float(value["rate"]), - key, - 1, - ] - ) - total_stake += cast(Balance, value["stake"]).tao - total_rate += float(value["rate"]) - metadata = { - "total_stake": "\u03c4{:.5f}".format(total_stake), - "total_balance": "\u03c4{:.5f}".format(total_balance), - "total_rate": "\u03c4{:.5f}/d".format(total_rate), - "rows": json.dumps(rows), - } - if not no_cache: - create_table( - "stakeshow", - [ - ("COLDKEY", "TEXT"), - ("BALANCE", "REAL"), - ("ACCOUNT", "TEXT"), - ("STAKE", "REAL"), - ("RATE", "REAL"), - ("HOTKEY", "TEXT"), - ("CHILD", "INTEGER"), - ], - db_rows, - ) - update_metadata_table("stakeshow", metadata) - else: - try: - metadata = get_metadata_table("stakeshow") - rows = json.loads(metadata["rows"]) - except sqlite3.OperationalError: - err_console.print( - "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " - "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " - "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " - "issue." - ) - return - if not html_output: - table = Table( - Column("[bold white]Coldkey", style="dark_orange", ratio=1), - Column( - "[bold white]Balance", - metadata["total_balance"], - style="dark_sea_green", - ratio=1, - ), - Column("[bold white]Account", style="bright_cyan", ratio=3), - Column("[bold white]Hotkey", ratio=7, no_wrap=True, style="bright_magenta"), - Column( - "[bold white]Stake", - metadata["total_stake"], - style="light_goldenrod2", - ratio=1, - ), - Column( - "[bold white]Rate /d", - metadata["total_rate"], - style="rgb(42,161,152)", - ratio=1, - ), - title=f"[underline dark_orange]Stake Show[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - ) - - for i, row in enumerate(rows): - is_last_row = i + 1 == len(rows) - table.add_row(*row) - - # If last row or new coldkey starting next - if is_last_row or (rows[i + 1][0] != ""): - table.add_row(end_section=True) - console.print(table) - - else: - render_tree( - "stakeshow", - f"Stakes | Total Balance: {metadata['total_balance']} - Total Stake: {metadata['total_stake']} " - f"Total Rate: {metadata['total_rate']}", - [ - {"title": "Coldkey", "field": "COLDKEY"}, - { - "title": "Balance", - "field": "BALANCE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Account", - "field": "ACCOUNT", - "width": 425, - }, - { - "title": "Stake", - "field": "STAKE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Daily Rate", - "field": "RATE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Hotkey", - "field": "HOTKEY", - "width": 425, - }, - ], - 0, - ) - - async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", @@ -1389,7 +1067,7 @@ async def stake_add( if not Confirm.ask("Would you like to continue?"): return False - async def send_extrinsic(netuid_i, amount_, current): + async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -1437,10 +1115,11 @@ async def send_extrinsic(netuid_i, amount_, current): wallet.unlock_coldkey() with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): extrinsics_coroutines = [ - send_extrinsic(ni, am, curr) + send_extrinsic(ni, am, curr, staking_address) for (ni, am, curr) in zip( netuids, stake_amount_balance, current_stake_balances ) + for _, staking_address in hotkeys_to_stake_to ] if len(extrinsics_coroutines) == 1: await asyncio.gather(*extrinsics_coroutines) @@ -1601,9 +1280,11 @@ async def unstake( async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): - sub_stakes = (await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address] - ))[wallet.coldkeypub.ss58_address] + sub_stakes = ( + await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address] + ) + )[wallet.coldkeypub.ss58_address] # Get registered delegates details. registered_delegate_info = await subtensor.get_delegate_identities() @@ -1620,115 +1301,174 @@ async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): # Iterate over substakes and aggregate them by hotkey. hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} - for substake in sub_stakes: - hotkey = substake.hotkey_ss58 - if substake.stake.rao == 0: - continue - if hotkey not in hotkeys_to_substakes: - hotkeys_to_substakes[hotkey] = [] - hotkeys_to_substakes[hotkey].append(substake) - - def table_substakes(hotkey: str, substakes: list[StakeInfo]): - # Create table structure. - name = registered_delegate_info[ - hotkey].name + f" ({hotkey})" if hotkey in registered_delegate_info else hotkey - rows = [] - total_global_tao = Balance(0) - total_tao_value = Balance(0) - for substake in substakes: - netuid = substake.netuid - pool = dynamic_info[netuid] - symbol = f"{bittensor.Balance.get_unit(netuid)}" - price = "{:.4f}{}".format(pool.price.__float__(), - f" τ/{Balance.get_unit(netuid)}\u200E") if pool.is_dynamic else ( - f" 1.0000 τ/{symbol} ") - alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid) - locked_value = Balance.from_rao(int(substake.locked.rao)).set_unit(netuid) - tao_value = pool.alpha_to_tao(alpha_value) - total_tao_value += tao_value - swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage(substake.stake) - if pool.is_dynamic: - slippage_percentage = 100 * float(slippage) / float( - slippage + swapped_tao_value) if slippage + swapped_tao_value != 0 else 0 - slippage_percentage = f"[dark_red]{slippage_percentage:.3f}%[/dark_red]" + def table_substakes(hotkey_: str, substakes: list[StakeInfo]): + # Create table structure. + name = ( + f"{registered_delegate_info[hotkey_].display} ({hotkey_})" + if hotkey_ in registered_delegate_info + else hotkey_ + ) + rows = [] + total_global_tao = Balance(0) + total_tao_value = Balance(0) + for substake_ in substakes: + netuid = substake_.netuid + pool = dynamic_info[netuid] + symbol = f"{Balance.get_unit(netuid)}" + # TODO: what is this price var for? + price = ( + "{:.4f}{}".format( + pool.price.__float__(), f" τ/{Balance.get_unit(netuid)}\u200e" + ) + if pool.is_dynamic + else (f" 1.0000 τ/{symbol} ") + ) + alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid) + locked_value = Balance.from_rao(int(substake_.locked.rao)).set_unit(netuid) + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value + swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( + substake_.stake + ) + if pool.is_dynamic: + slippage_percentage_ = ( + 100 * float(slippage) / float(slippage + swapped_tao_value) + if slippage + swapped_tao_value != 0 + else 0 + ) + slippage_percentage = ( + f"[dark_red]{slippage_percentage_:.3f}%[/dark_red]" + ) + else: + slippage_percentage = "0.000%" + tao_locked = pool.tao_in + issuance = pool.alpha_out if pool.is_dynamic else tao_locked + per_block_emission = substake_.emission.tao / ( + (emission_drain_tempo / pool.tempo) * pool.tempo + ) + if alpha_value.tao > 0.00009: + if issuance.tao != 0: + alpha_ownership = "{:.4f}".format( + (alpha_value.tao / issuance.tao) * 100 + ) + tao_ownership = Balance.from_tao( + (alpha_value.tao / issuance.tao) * tao_locked.tao + ) + total_global_tao += tao_ownership else: - slippage_percentage = '0.000%' - tao_locked = pool.tao_in - issuance = pool.alpha_out if pool.is_dynamic else tao_locked - per_block_emission = substake.emission.tao / ((emission_drain_tempo / pool.tempo) * pool.tempo) - if alpha_value.tao > 0.00009: - if issuance.tao != 0: - alpha_ownership = "{:.4f}".format((alpha_value.tao / issuance.tao) * 100) - tao_ownership = bittensor.Balance.from_tao((alpha_value.tao / issuance.tao) * tao_locked.tao) - total_global_tao += tao_ownership - else: - alpha_ownership = "0.0000" - tao_ownership = "0.0000" - rows.append([ + # TODO what's this var for? + alpha_ownership = "0.0000" + tao_ownership = "0.0000" + rows.append( + [ str(netuid), # Number symbol, # Symbol # f"[medium_purple]{tao_ownership}[/medium_purple] ([light_salmon3]{ alpha_ownership }[/light_salmon3][white]%[/white])", # Tao ownership. f"[medium_purple]{tao_ownership}[/medium_purple]", # Tao ownership. # f"[dark_sea_green]{ alpha_value }", # Alpha value - f"{substake.stake.tao:,.4f} {symbol}", + f"{substake_.stake.tao:,.4f} {symbol}", f"{pool.price.tao:.4f} τ/{symbol}", f"[light_slate_blue]{tao_value}[/light_slate_blue]", # Tao equiv f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap amount. # f"[light_salmon3]{ alpha_ownership }%[/light_salmon3]", # Ownership. - f"[bold cadet_blue]YES[/bold cadet_blue]" if substake.is_registered else f"[dark_red]NO[/dark_red]", + "[bold cadet_blue]YES[/bold cadet_blue]" + if substake_.is_registered + else "[dark_red]NO[/dark_red]", # Registered. - str(bittensor.Balance.from_tao(per_block_emission).set_unit( - netuid)) if substake.is_registered else "[dark_red]N/A[/dark_red]", # emission per block. + str(Balance.from_tao(per_block_emission).set_unit(netuid)) + if substake_.is_registered + else "[dark_red]N/A[/dark_red]", # emission per block. f"[light_slate_blue]{locked_value}[/light_slate_blue]", # Locked value - ]) - # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") - table = Table( - title=f"[white]hotkey:[/white] [light_salmon3]{name}[/light_salmon3]\n", - width=bittensor.__console__.width - 5, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, - show_footer=True, - show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", - title_justify="center", - highlight=False, - ) - table.add_column("[white]Netuid", footer_style="overline white", style="grey89") - table.add_column("[white]Symbol", footer_style="white", style="light_goldenrod1", justify="right", - width=5, - no_wrap=True) - table.add_column(f"[white]TAO({Balance.unit})", style="aquamarine3", justify="right", - footer=f"{total_global_tao}") - table.add_column(f"[white]Stake({Balance.get_unit(1)})", footer_style="overline white", - style="green", justify="right") - table.add_column(f"[white]Rate({Balance.unit}/{Balance.get_unit(1)})", - footer_style="white", style="light_goldenrod2", justify="center") - table.add_column( - f"[white]Value({bittensor.Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", - footer_style="overline white", style="blue", justify="right", footer=f"{total_tao_value}") - table.add_column(f"[white]Swap({Balance.get_unit(1)}) -> {Balance.unit}", - footer_style="overline white", style="white", justify="right") - # table.add_column(f"[white]Control({bittensor.Balance.get_unit(1)})", style="aquamarine3", justify="right") - table.add_column("[white]Registered", style="red", justify="right") - table.add_column(f"[white]Emission({Balance.get_unit(1)}/block)", style="aquamarine3", - justify="right") - table.add_column(f"[white]Locked({Balance.get_unit(1)})", footer_style="overline white", - style="green", justify="right") - for row in rows: - table.add_row(*row) - console.print(table) - return total_global_tao, total_tao_value + ] + ) + # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") + table = Table( + title=f"[white]hotkey:[/white] [light_salmon3]{name}[/light_salmon3]\n", + width=console.width - 5, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + table.add_column("[white]Netuid", footer_style="overline white", style="grey89") + table.add_column( + "[white]Symbol", + footer_style="white", + style="light_goldenrod1", + justify="right", + width=5, + no_wrap=True, + ) + table.add_column( + f"[white]TAO({Balance.unit})", + style="aquamarine3", + justify="right", + footer=f"{total_global_tao}", + ) + table.add_column( + f"[white]Stake({Balance.get_unit(1)})", + footer_style="overline white", + style="green", + justify="right", + ) + table.add_column( + f"[white]Rate({Balance.unit}/{Balance.get_unit(1)})", + footer_style="white", + style="light_goldenrod2", + justify="center", + ) + table.add_column( + f"[white]Value({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + footer_style="overline white", + style="blue", + justify="right", + footer=f"{total_tao_value}", + ) + table.add_column( + f"[white]Swap({Balance.get_unit(1)}) -> {Balance.unit}", + footer_style="overline white", + style="white", + justify="right", + ) + # table.add_column(f"[white]Control({bittensor.Balance.get_unit(1)})", style="aquamarine3", justify="right") + table.add_column("[white]Registered", style="red", justify="right") + table.add_column( + f"[white]Emission({Balance.get_unit(1)}/block)", + style="aquamarine3", + justify="right", + ) + table.add_column( + f"[white]Locked({Balance.get_unit(1)})", + footer_style="overline white", + style="green", + justify="right", + ) + for row in rows: + table.add_row(*row) + console.print(table) + return total_global_tao, total_tao_value + + for substake in sub_stakes: + hotkey = substake.hotkey_ss58 + if substake.stake.rao == 0: + continue + if hotkey not in hotkeys_to_substakes: + hotkeys_to_substakes[hotkey] = [] + hotkeys_to_substakes[hotkey].append(substake) # Iterate over each hotkey and make a table all_hotkeys_total_global_tao = Balance(0) @@ -1740,7 +1480,12 @@ def table_substakes(hotkey: str, substakes: list[StakeInfo]): console.print("\n\n") console.print( - f"Wallet:\n Coldkey SS58: [bold dark_green]{cli.config.coldkey_address}[/bold dark_green]\n Free Balance: [aquamarine3]{balance}[/aquamarine3]\n Total TAO ({bittensor.Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n Total Value ({bittensor.Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]") + f"Wallet:\n" + f" Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n" + f" Free Balance: [aquamarine3]{balance}[/aquamarine3]\n" + f" Total TAO ({Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n" + f" Total Value ({Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]" + ) console.print( """ [bold white]Description[/bold white]: @@ -1758,4 +1503,4 @@ def table_substakes(hotkey: str, substakes: list[StakeInfo]): - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). """ - ) \ No newline at end of file + ) From 0c0c2df3452538da03123b0c38a0ff05d403babf Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 17 Oct 2024 17:10:52 +0200 Subject: [PATCH 024/332] Removed root app/commands. --- bittensor_cli/cli.py | 801 +------------ bittensor_cli/src/commands/root.py | 1770 ---------------------------- 2 files changed, 1 insertion(+), 2570 deletions(-) delete mode 100644 bittensor_cli/src/commands/root.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 11ba38e0..8566a180 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -30,7 +30,7 @@ from bittensor_cli.src.bittensor.async_substrate_interface import ( SubstrateRequestException, ) -from bittensor_cli.src.commands import root, subnets, sudo, wallets +from bittensor_cli.src.commands import subnets, sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.stake import children_hotkeys, stake from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -418,7 +418,6 @@ class CLIManager: :var app: the main CLI Typer app :var config_app: the Typer app as it relates to config commands :var wallet_app: the Typer app as it relates to wallet commands - :var root_app: the Typer app as it relates to root commands :var stake_app: the Typer app as it relates to stake commands :var sudo_app: the Typer app as it relates to sudo commands :var subnets_app: the Typer app as it relates to subnets commands @@ -429,7 +428,6 @@ class CLIManager: app: typer.Typer config_app: typer.Typer wallet_app: typer.Typer - root_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer utils_app = typer.Typer(epilog=_epilog) @@ -471,7 +469,6 @@ def __init__(self): ) self.config_app = typer.Typer(epilog=_epilog) self.wallet_app = typer.Typer(epilog=_epilog) - self.root_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) @@ -501,15 +498,6 @@ def __init__(self): self.wallet_app, name="wallets", hidden=True, no_args_is_help=True ) - # root aliases - self.app.add_typer( - self.root_app, - name="root", - short_help="Root commands, alias: `r`", - no_args_is_help=True, - ) - self.app.add_typer(self.root_app, name="r", hidden=True, no_args_is_help=True) - # stake aliases self.app.add_typer( self.stake_app, @@ -619,49 +607,6 @@ def __init__(self): "sign", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_sign) - # root commands - self.root_app.command("list")(self.root_list) - self.root_app.command( - "set-weights", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_set_weights) - self.root_app.command( - "get-weights", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_get_weights) - self.root_app.command( - "boost", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_boost) - self.root_app.command( - "slash", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_slash) - self.root_app.command( - "senate", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_senate) - self.root_app.command( - "senate-vote", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_senate_vote) - self.root_app.command("register")(self.root_register) - self.root_app.command( - "proposals", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_proposals) - self.root_app.command( - "set-take", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_set_take) - self.root_app.command( - "delegate-stake", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_delegate_stake) - self.root_app.command( - "undelegate-stake", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_undelegate_stake) - self.root_app.command( - "my-delegates", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_my_delegates) - self.root_app.command( - "list-delegates", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_list_delegates) - self.root_app.command( - "nominate", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_nominate) - # stake commands self.stake_app.command( "add", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] @@ -761,18 +706,6 @@ def __init__(self): hidden=True, )(self.wallet_get_id) - # Root - self.root_app.command("set_weights", hidden=True)(self.root_set_weights) - self.root_app.command("get_weights", hidden=True)(self.root_get_weights) - self.root_app.command("senate_vote", hidden=True)(self.root_senate_vote) - self.root_app.command("set_take", hidden=True)(self.root_set_take) - self.root_app.command("delegate_stake", hidden=True)(self.root_delegate_stake) - self.root_app.command("undelegate_stake", hidden=True)( - self.root_undelegate_stake - ) - self.root_app.command("my_delegates", hidden=True)(self.root_my_delegates) - self.root_app.command("list_delegates", hidden=True)(self.root_list_delegates) - # Subnets self.subnets_app.command("lock_cost", hidden=True)(self.subnets_lock_cost) self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) @@ -2395,738 +2328,6 @@ def wallet_sign( return self._run_command(wallets.sign(wallet, message, use_hotkey)) - def root_list( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Show the neurons (root network validators) in the root network (netuid = 0). - - USAGE - - The command fetches and lists the neurons (root network validators) in the root network, showing their unique identifiers (UIDs), names, addresses, stakes, and whether they are part of the senate (network governance body). - - This command is useful for understanding the composition and governance structure of the Bittensor network's root network. It provides insights into which neurons hold significant influence and responsibility within the Bittensor network. - - EXAMPLE - - [green]$[/green] btcli root list - """ - self.verbosity_handler(quiet, verbose) - return self._run_command( - root.root_list(subtensor=self.initialize_chain(network)) - ) - - def root_set_weights( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuids=typer.Option( - None, - "--netuids", - "--netuid", - "-n", - help="Set the netuid(s) to set weights to. Separate multiple netuids with a comma, for example: `-n 0,1,2`.", - ), - weights: str = Options.weights, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Set the weights for different subnets, by setting them in the root network. - - To use this command, you should specify the netuids and corresponding weights you wish to assign. This command is used by validators registered to the root subnet to influence the distribution of subnet rewards and responsibilities. - - You must have a comprehensive understanding of the dynamics of the subnets to use this command. It is a powerful tool that directly impacts the subnet's operational mechanics and reward distribution. - - EXAMPLE - - With no spaces between the passed values: - - [green]$[/green] btcli root set-weights --netuids 1,2 --weights 0.2,0.3 - - or - - Include double quotes to include spaces between the passed values: - - [green]$[/green] btcli root set-weights --netuids "1, 2" --weights "0.2, 0.3" - """ - self.verbosity_handler(quiet, verbose) - - if netuids: - netuids = parse_to_list( - netuids, - int, - "Netuids must be a comma-separated list of ints, e.g., `--netuid 1,2,3,4`.", - ) - else: - netuids = list_prompt(netuids, int, "Enter netuids (e.g: 1, 4, 6)") - - if weights: - weights = parse_to_list( - weights, - float, - "Weights must be a comma-separated list of floats, e.g., `--weights 0.3,0.4,0.3`.", - ) - else: - weights = list_prompt( - weights, float, "Enter weights (e.g. 0.02, 0.03, 0.01)" - ) - - if len(netuids) != len(weights): - raise typer.BadParameter( - "The number of netuids and weights must be the same." - ) - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.HOTKEY, WO.PATH, WO.NAME], - validate=WV.WALLET_AND_HOTKEY, - ) - self._run_command( - root.set_weights( - wallet, self.initialize_chain(network), netuids, weights, prompt - ) - ) - - def root_get_weights( - self, - network: Optional[list[str]] = Options.network, - limit_min_col: Optional[int] = typer.Option( - None, - "--limit-min-col", - "--min", - help="Limit the left display of the table to this column.", - ), - limit_max_col: Optional[int] = typer.Option( - None, - "--limit-max-col", - "--max", - help="Limit the right display of the table to this column.", - ), - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows a table listing the weights assigned to each subnet in the root network. - - This command provides visibility into how network responsibilities and rewards are distributed among various subnets. This information is crucial for understanding the current influence and reward distribution across different subnets. Use this command if you are interested in the governance and operational dynamics of the Bittensor network. - - EXAMPLE - - [green]$[/green] btcli root get_weights - """ - self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: - err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'." - "Change it to 'False' using `btcli config set`." - ) - raise typer.Exit() - if not reuse_last: - subtensor = self.initialize_chain(network) - else: - subtensor = None - return self._run_command( - root.get_weights( - subtensor, - limit_min_col, - limit_max_col, - reuse_last, - html_output, - not self.config.get("use_cache", True), - ) - ) - - def root_boost( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - netuid: int = Options.netuid, - amount: float = typer.Option( - None, - "--amount", - "--increase", - "-a", - prompt="Enter the boost amount (added to existing weight)", - help="Amount (float) to boost (added to the existing weight), (e.g. 0.01)", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Increase (boost) the weights for a specific subnet in the root network. Any amount provided will be added to the subnet's existing weight. - - EXAMPLE - - [green]$[/green] btcli root boost --netuid 1 --increase 0.01 - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.set_boost( - wallet, self.initialize_chain(network), netuid, amount, prompt - ) - ) - - def root_slash( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - netuid: int = Options.netuid, - amount: float = typer.Option( - None, - "--amount", - "--decrease", - "-a", - prompt="Enter the slash amount (subtracted from the existing weight)", - help="Amount (float) to slash (subtract from the existing weight), (e.g. 0.01)", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Decrease (slash) the weights for a specific subnet in the root network. Any amount provided will be subtracted from the subnet's existing weight. - - EXAMPLE - - [green]$[/green] btcli root slash --netuid 1 --decrease 0.01 - - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.set_slash( - wallet, self.initialize_chain(network), netuid, amount, prompt - ) - ) - - def root_senate_vote( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - proposal: str = typer.Option( - None, - "--proposal", - "--proposal-hash", - prompt="Enter the proposal hash", - help="The hash of the proposal to vote on.", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - vote: bool = typer.Option( - None, - "--vote-aye/--vote-nay", - prompt="Enter y to vote Aye, or enter n to vote Nay", - help="The vote casted on the proposal", - ), - ): - """ - Cast a vote on an active proposal in Bittensor's governance protocol. - - This command is used by Senate members to vote on various proposals that shape the network's future. Use `btcli root proposals` to see the active proposals and their hashes. - - USAGE - - The user must specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast a 'Yes' or 'No' vote, contributing to the decision-making process on the proposal. This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. - - EXAMPLE - - [green]$[/green] btcli root senate_vote --proposal - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.senate_vote( - wallet, self.initialize_chain(network), proposal, vote, prompt - ) - ) - - def root_senate( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows the Senate members of the Bittensor's governance protocol. - - This command lists the delegates involved in the decision-making process of the Bittensor network, showing their names and wallet addresses. This information is crucial for understanding who holds governance roles within the network. - - EXAMPLE - - [green]$[/green] btcli root senate - """ - self.verbosity_handler(quiet, verbose) - return self._run_command(root.get_senate(self.initialize_chain(network))) - - def root_register( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Register a neuron to the root subnet by recycling some TAO to cover for the registration cost. - - This command adds a new neuron as a validator on the root network. This will allow the neuron owner to set subnet weights. - - # Usage: - - Before registering, the command checks if the specified subnet exists and whether the TAO balance in the user's wallet is sufficient to cover the registration cost. The registration cost is determined by the current recycle amount for the specified subnet. If the balance is insufficient or the subnet does not exist, the command will exit with an appropriate error message. - - # Example usage: - - [green]$[/green] btcli subnets register --netuid 1 - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.register(wallet, self.initialize_chain(network), prompt) - ) - - def root_proposals( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - View active proposals for the senate in the Bittensor's governance protocol. - - This command displays the details of ongoing proposals, including proposal hashes, votes, thresholds, and proposal data. - - EXAMPLE - - [green]$[/green] btcli root proposals - """ - self.verbosity_handler(quiet, verbose) - return self._run_command(root.proposals(self.initialize_chain(network))) - - def root_set_take( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - take: float = typer.Option(None, help="The new take value."), - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Allows users to change their delegate take percentage. - - This command can be used to update the delegate takes individually for every subnet. To run the command, the user must have a configured wallet with both hotkey and coldkey. The command performs the below checks: - - 1. The provided hotkey is already a delegate. - 2. The new take value is within 0-18% range. - - EXAMPLE - - [green]$[/green] btcli root set_take --wallet-name my_wallet --wallet-hotkey my_hotkey - """ - max_value = 0.18 - min_value = 0.00 - self.verbosity_handler(quiet, verbose) - - if not take: - max_value_style = typer.style(f"Max: {max_value}", fg="magenta") - min_value_style = typer.style(f"Min: {min_value}", fg="magenta") - prompt_text = typer.style( - "Enter take value (0.18 for 18%)", fg="bright_cyan", bold=True - ) - take = FloatPrompt.ask(f"{prompt_text} {min_value_style} {max_value_style}") - - if not (min_value <= take <= max_value): - print_error( - f"Take value must be between {min_value} and {max_value}. Provided value: {take}" - ) - raise typer.Exit() - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - - return self._run_command( - root.set_take(wallet, self.initialize_chain(network), take) - ) - - def root_delegate_stake( - self, - delegate_ss58key: str = typer.Option( - None, - help="The ss58 address of the delegate hotkey to stake TAO to.", - prompt="Enter the hotkey ss58 address you want to delegate TAO to.", - ), - amount: Optional[float] = typer.Option( - None, help="The amount of TAO to stake. Do no specify if using `--all`" - ), - stake_all: Optional[bool] = typer.Option( - False, - "--all", - "-a", - help="If specified, the command stakes all available TAO. Do not specify if using" - " `--amount`", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Stakes TAO to a specified delegate hotkey. - - This command allocates the user's TAO to the specified hotkey of a delegate, potentially earning staking rewards in return. If the - `--all` flag is used, it delegates the entire TAO balance available in the user's wallet. - - This command should be run by a TAO holder. Compare this command with "btcli stake add" that is typically run by a subnet validator to add stake to their own delegate hotkey. - - EXAMPLE - - [green]$[/green] btcli root delegate-stake --delegate_ss58key --amount - - [green]$[/green] btcli root delegate-stake --delegate_ss58key --all - - [blue bold]Note[/blue bold]: This command modifies the blockchain state and may incur transaction fees. The user should ensure the delegate's address and the amount to be staked are correct before executing the command. - """ - self.verbosity_handler(quiet, verbose) - if amount and stake_all: - err_console.print( - "Both `--amount` and `--all` are specified. Choose one or the other." - ) - if not stake_all and not amount: - while True: - amount = FloatPrompt.ask( - "[blue bold]Amount to stake (TAO τ)[/blue bold]", console=console - ) - confirmation = FloatPrompt.ask( - "[blue bold]Confirm the amount to stake (TAO τ)[/blue bold]", - console=console, - ) - if amount == confirmation: - break - else: - err_console.print( - "[red]The amounts do not match. Please try again.[/red]" - ) - - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - return self._run_command( - root.delegate_stake( - wallet, - self.initialize_chain(network), - amount, - delegate_ss58key, - prompt, - ) - ) - - def root_undelegate_stake( - self, - delegate_ss58key: str = typer.Option( - None, - help="The ss58 address of the delegate to undelegate from.", - prompt="Enter the hotkey ss58 address you want to undelegate from", - ), - amount: Optional[float] = typer.Option( - None, help="The amount of TAO to unstake. Do no specify if using `--all`" - ), - unstake_all: Optional[bool] = typer.Option( - False, - "--all", - "-a", - help="If specified, the command undelegates all staked TAO from the delegate. Do not specify if using" - " `--amount`", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Allows users to withdraw their staked TAO from a delegate. - - The user must provide the delegate hotkey's ss58 address and the amount of TAO to undelegate. The function will then send a transaction to the blockchain to process the undelegation. This command can result in a change to the blockchain state and may incur transaction fees. - - EXAMPLE - - [green]$[/green] btcli undelegate --delegate_ss58key --amount - - [green]$[/green] btcli undelegate --delegate_ss58key --all - """ - self.verbosity_handler(quiet, verbose) - if amount and unstake_all: - err_console.print( - "Both `--amount` and `--all` are specified. Choose one or the other." - ) - if not unstake_all and not amount: - while True: - amount = FloatPrompt.ask( - "[blue bold]Amount to unstake (TAO τ)[/blue bold]", console=console - ) - confirmation = FloatPrompt.ask( - "[blue bold]Confirm the amount to unstake (TAO τ)[/blue bold]", - console=console, - ) - if amount == confirmation: - break - else: - err_console.print( - "[red]The amounts do not match. Please try again.[/red]" - ) - - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - self._run_command( - root.delegate_unstake( - wallet, - self.initialize_chain(network), - amount, - delegate_ss58key, - prompt, - ) - ) - - def root_my_delegates( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - all_wallets: bool = typer.Option( - False, - "--all-wallets", - "--all", - "-a", - help="If specified, the command aggregates information across all the wallets.", - ), - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows a table with the details on the user's delegates. - - The table output includes the following columns: - - - Wallet: The name of the user's wallet (coldkey). - - - OWNER: The name of the delegate who owns the hotkey. - - - SS58: The truncated SS58 address of the delegate's hotkey. - - - Delegation: The amount of TAO staked by the user to the delegate. - - - τ/24h: The earnings from the delegate to the user over the past 24 hours. - - - NOMS: The number of nominators for the delegate. - - - OWNER STAKE(τ): The stake amount owned by the delegate. - - - TOTAL STAKE(τ): The total stake amount held by the delegate. - - - SUBNETS: The list of subnets the delegate is a part of. - - - VPERMIT: Validator permits held by the delegate for various subnets. - - - 24h/kτ: Earnings per 1000 TAO staked over the last 24 hours. - - - Desc: A description of the delegate. - - The command also sums and prints the total amount of TAO delegated across all wallets. - - EXAMPLE - - [green]$[/green] btcli root my-delegates - [green]$[/green] btcli root my-delegates --all - [green]$[/green] btcli root my-delegates --wallet-name my_wallet - - [blue bold]Note[/blue bold]: This command is not intended to be used directly in user code. - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=([WO.NAME, WO.PATH] if not all_wallets else [WO.PATH]), - validate=WV.WALLET if not all_wallets else WV.NONE, - ) - self._run_command( - root.my_delegates(wallet, self.initialize_chain(network), all_wallets) - ) - - def root_list_delegates( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Displays a table of Bittensor network-wide delegates, providing a comprehensive overview of delegate statistics and information. - - This table helps users make informed decisions on which delegates to allocate their TAO stake. - - The table columns include: - - - INDEX: The delegate's index in the sorted list. - - - DELEGATE: The name of the delegate. - - - SS58: The delegate's unique ss58 address (truncated for display). - - - NOMINATORS: The count of nominators backing the delegate. - - - OWN STAKE(τ): The amount of delegate's own stake (not the TAO delegated from any nominators). - - - TOTAL STAKE(τ): The delegate's total stake, i.e., the sum of delegate's own stake and nominators' stakes. - - - CHANGE/(4h): The percentage change in the delegate's stake over the last four hours. - - - SUBNETS: The subnets in which the delegate is registered. - - - VPERMIT: Indicates the subnets in which the delegate has validator permits. - - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - - DELEGATE/(24h): The total earnings of the delegate in the last 24 hours. - - - DESCRIPTION: A brief description of the delegate's purpose and operations. - - [blue bold]NOTES:[/blue bold] - - - Sorting is done based on the `TOTAL STAKE` column in descending order. - - Changes in stake are shown as: increases in green and decreases in red. - - Entries with no previous data are marked with `NA`. - - Each delegate's name is a hyperlink to more information, if available. - - EXAMPLE - - [green]$[/green] btcli root list_delegates - - [green]$[/green] btcli root list_delegates --subtensor.network finney # can also be `test` or `local` - - [blue bold]NOTE[/blue bold]: This command is intended for use within a - console application. It prints directly to the console and does not return any value. - """ - self.verbosity_handler(quiet, verbose) - - if network: - if "finney" in network: - network = ["wss://archive.chain.opentensor.ai:443"] - elif (conf_net := self.config.get("network")) == "finney": - network = ["wss://archive.chain.opentensor.ai:443"] - elif conf_net: - network = [conf_net] - else: - network = ["wss://archive.chain.opentensor.ai:443"] - - sub = self.initialize_chain(network) - return self._run_command(root.list_delegates(sub)) - - # TODO: Confirm if we need a command for this - currently registering to root auto makes u delegate - def root_nominate( - self, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Enables a wallet's hotkey to become a delegate. - - This command handles the nomination process, including wallet unlocking and verification of the hotkey's current delegate status. - - The command performs several checks: - - - Verifies that the hotkey is not already a delegate to prevent redundant nominations. - - - Tries to nominate the wallet and reports success or failure. - - Upon success, the wallet's hotkey is registered as a delegate on the network. - - To run the command, the user must have a configured wallet with both hotkey and coldkey. If the wallet is not already nominated, this command will initiate the process. - - EXAMPLE - - [green]$[/green] btcli root nominate - - [green]$[/green] btcli root nominate --wallet-name my_wallet --wallet-hotkey my_hotkey - - [blue bold]Note[/blue bold]: This command prints the output directly to the console. It should not be called programmatically in user code due to its interactive nature and side effects on the network state. - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.nominate(wallet, self.initialize_chain(network), prompt) - ) - def stake_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py deleted file mode 100644 index 2401eb00..00000000 --- a/bittensor_cli/src/commands/root.py +++ /dev/null @@ -1,1770 +0,0 @@ -import asyncio -import json -from typing import Optional, TYPE_CHECKING - -from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError -import numpy as np -from numpy.typing import NDArray -from rich import box -from rich.prompt import Confirm -from rich.table import Column, Table -from rich.text import Text -from scalecodec import GenericCall, ScaleType -from substrateinterface.exceptions import SubstrateRequestException -import typer - -from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import ( - DelegateInfo, - NeuronInfoLite, - decode_account_id, -) -from bittensor_cli.src.bittensor.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) -from bittensor_cli.src.commands.wallets import ( - get_coldkey_wallets_for_path, - set_id, - set_id_prompts, -) -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -from bittensor_cli.src.bittensor.utils import ( - console, - convert_weight_uids_and_vals_to_tensor, - create_table, - err_console, - print_verbose, - get_metadata_table, - render_table, - ss58_to_vec_u8, - update_metadata_table, - group_subnets, -) - -if TYPE_CHECKING: - from bittensor_cli.src.bittensor.subtensor_interface import ProposalVoteData - -# helpers - - -def display_votes( - vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] -) -> str: - vote_list = list() - - for address in vote_data.ayes: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold green]Aye[/bold green]", - ) - ) - - for address in vote_data.nays: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold red]Nay[/bold red]", - ) - ) - - return "\n".join(vote_list) - - -def format_call_data(call_data: dict) -> str: - # Extract the module and call details - module, call_details = next(iter(call_data.items())) - - # Extract the call function name and arguments - call_info = call_details[0] - call_function, call_args = next(iter(call_info.items())) - - # Extract the argument, handling tuple values - formatted_args = ", ".join( - str(arg[0]) if isinstance(arg, tuple) else str(arg) - for arg in call_args.values() - ) - - # Format the final output string - return f"{call_function}({formatted_args})" - - -async def _get_senate_members( - subtensor: SubtensorInterface, block_hash: Optional[str] = None -) -> list[str]: - """ - Gets all members of the senate on the given subtensor's network - - :param subtensor: SubtensorInterface object to use for the query - - :return: list of the senate members' ss58 addresses - """ - senate_members = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - block_hash=block_hash, - ) - try: - return [ - decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) - ] - except (IndexError, TypeError): - err_console.print("Unable to retrieve senate members.") - return [] - - -async def _get_proposals( - subtensor: SubtensorInterface, block_hash: str -) -> dict[str, tuple[dict, "ProposalVoteData"]]: - async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: - proposal_data = await subtensor.substrate.query( - module="Triumvirate", - storage_function="ProposalOf", - block_hash=block_hash, - params=[p_hash], - ) - return proposal_data - - ph = await subtensor.substrate.query( - module="Triumvirate", - storage_function="Proposals", - params=None, - block_hash=block_hash, - ) - - try: - proposal_hashes: list[str] = [ - f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0])) - ] - except (IndexError, TypeError): - err_console.print("Unable to retrieve proposal vote data") - return {} - - call_data_, vote_data_ = await asyncio.gather( - asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]), - asyncio.gather(*[subtensor.get_vote_data(h) for h in proposal_hashes]), - ) - return { - proposal_hash: (cd, vd) - for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes) - } - - -def _validate_proposal_hash(proposal_hash: str) -> bool: - if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66: - return False - else: - return True - - -async def _is_senate_member(subtensor: SubtensorInterface, hotkey_ss58: str) -> bool: - """ - Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. - The senate is a key governance body within the Bittensor network, responsible for overseeing and - approving various network operations and proposals. - - :param subtensor: SubtensorInterface object to use for the query - :param hotkey_ss58: The `SS58` address of the neuron's hotkey. - - :return: `True` if the neuron is a senate member at the given block, `False` otherwise. - - This function is crucial for understanding the governance dynamics of the Bittensor network and for - identifying the neurons that hold decision-making power within the network. - """ - - senate_members = await _get_senate_members(subtensor) - - if not hasattr(senate_members, "count"): - return False - - return senate_members.count(hotkey_ss58) > 0 - - -async def vote_senate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Votes ayes or nays on proposals. - - :param subtensor: The SubtensorInterface object to use for the query - :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. - :param proposal_hash: The hash of the proposal for which voting data is requested. - :param proposal_idx: The index of the proposal to vote. - :param vote: Whether to vote aye or nay. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Cast a vote of {vote}?"): - return False - - with console.status(":satellite: Casting vote..", spinner="aesthetic"): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="vote", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "proposal": proposal_hash, - "index": proposal_idx, - "approve": vote, - }, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) - return False - # Successful vote, final check for data - else: - if vote_data := await subtensor.get_vote_data(proposal_hash): - if ( - vote_data.ayes.count(wallet.hotkey.ss58_address) > 0 - or vote_data.nays.count(wallet.hotkey.ss58_address) > 0 - ): - console.print(":white_heavy_check_mark: [green]Vote cast.[/green]") - return True - else: - # hotkey not found in ayes/nays - err_console.print( - ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" - ) - return False - else: - return False - - -async def burned_register_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - netuid: int, - recycle_amount: Balance, - old_balance: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers the wallet to chain by recycling TAO. - - :param subtensor: The SubtensorInterface object to use for the call, initialized - :param wallet: Bittensor wallet object. - :param netuid: The `netuid` of the subnet to register on. - :param recycle_amount: The amount of TAO required for this burn. - :param old_balance: The wallet balance prior to the registration burn. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - with console.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", - spinner="aesthetic", - ) as status: - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ) - - print_verbose("Checking if already registered", status) - neuron = await subtensor.neuron_for_uid( - uid=my_uid, - netuid=netuid, - block_hash=subtensor.substrate.last_block_hash, - ) - - if not neuron.is_null: - console.print( - ":white_heavy_check_mark: [green]Already Registered[/green]:\n" - f"uid: [bold white]{neuron.uid}[/bold white]\n" - f"netuid: [bold white]{neuron.netuid}[/bold white]\n" - f"hotkey: [bold white]{neuron.hotkey}[/bold white]\n" - f"coldkey: [bold white]{neuron.coldkey}[/bold white]" - ) - return True - - with console.status( - ":satellite: Recycling TAO for Registration...", spinner="aesthetic" - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) - return False - # Successful registration, final check for neuron and pubkey - else: - with console.status(":satellite: Checking Balance...", spinner="aesthetic"): - block_hash = await subtensor.substrate.get_chain_head() - new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, - block_hash=block_hash, - reuse_block=False, - ), - subtensor.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, block_hash=block_hash - ), - subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ), - ) - - console.print( - "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" - ) - - if len(netuids_for_hotkey) > 0: - console.print( - f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" - ) - return True - else: - # neuron not found, try again - err_console.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - return False - - -async def set_take_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - delegate_ss58: str, - take: float = 0.0, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Set delegate hotkey take - - :param subtensor: SubtensorInterface (initialized) - :param wallet: The wallet containing the hotkey to be nominated. - :param delegate_ss58: Hotkey - :param take: Delegate take on subnet ID - :param wait_for_finalization: If `True`, waits until the transaction is finalized on the - blockchain. - :param wait_for_inclusion: If `True`, waits until the transaction is included in a block. - - :return: `True` if the process is successful, `False` otherwise. - - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - - async def _get_delegate_by_hotkey(ss58: str) -> Optional[DelegateInfo]: - """Retrieves the delegate info for a given hotkey's ss58 address""" - encoded_hotkey = ss58_to_vec_u8(ss58) - json_body = await subtensor.substrate.rpc_request( - method="delegateInfo_getDelegate", # custom rpc method - params=([encoded_hotkey, subtensor.substrate.last_block_hash]), - ) - if not (result := json_body.get("result", None)): - return None - else: - return DelegateInfo.from_vec_u8(bytes(result)) - - # Calculate u16 representation of the take - take_u16 = int(take * 0xFFFF) - - print_verbose("Checking current take") - # Check if the new take is greater or lower than existing take or if existing is set - delegate = await _get_delegate_by_hotkey(delegate_ss58) - current_take = None - if delegate is not None: - current_take = int( - float(delegate.take) * 65535.0 - ) # TODO verify this, why not u16_float_to_int? - - if take_u16 == current_take: - console.print("Nothing to do, take hasn't changed") - return True - if current_take is None or current_take < take_u16: - console.print( - f"Current take is {float(delegate.take):.4f}. Increasing to {take:.4f}." - ) - with console.status( - f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="increase_take", - call_params={ - "hotkey": delegate_ss58, - "take": take_u16, - }, - ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) - - else: - console.print( - f"Current take is {float(delegate.take):.4f}. Decreasing to {take:.4f}." - ) - with console.status( - f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="decrease_take", - call_params={ - "hotkey": delegate_ss58, - "take": take_u16, - }, - ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) - - if not success: - err_console.print(err) - else: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - return success - - -async def delegate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - delegate_ss58: str, - amount: Optional[float], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - delegate: bool = True, -) -> bool: - """Delegates the specified amount of stake to the passed delegate. - - :param subtensor: The SubtensorInterface used to perform the delegation, initialized. - :param wallet: Bittensor wallet object. - :param delegate_ss58: The `ss58` address of the delegate. - :param amount: Amount to stake as bittensor balance, None to stake all available TAO. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :param delegate: whether to delegate (`True`) or undelegate (`False`) - - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. - """ - - async def _do_delegation(staking_balance_: Balance) -> tuple[bool, str]: - """Performs the delegation extrinsic call to the chain.""" - if delegate: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_staked": staking_balance_.rao, - }, - ) - else: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_unstaked": staking_balance_.rao, - }, - ) - return await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - async def get_hotkey_owner(ss58: str, block_hash_: str): - """Returns the coldkey owner of the passed hotkey.""" - if not await subtensor.does_hotkey_exist(ss58, block_hash=block_hash_): - return None - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[ss58], - block_hash=block_hash_, - ) - return decode_account_id(_result[0]) - - async def get_stake_for_coldkey_and_hotkey( - hotkey_ss58: str, coldkey_ss58: str, block_hash_: str - ): - """Returns the stake under a coldkey - hotkey pairing.""" - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Stake", - params=[hotkey_ss58, coldkey_ss58], - block_hash=block_hash_, - ) - return Balance.from_rao(_result or 0) - - delegate_string = "delegate" if delegate else "undelegate" - - # Decrypt key - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - print_verbose("Checking if hotkey is a delegate") - if not await subtensor.is_hotkey_delegate(delegate_ss58): - err_console.print(f"Hotkey: {delegate_ss58} is not a delegate.") - return False - - # Get state. - with console.status( - f":satellite: Syncing with [bold white]{subtensor}[/bold white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching balance, stake, and ownership", status) - initial_block_hash = await subtensor.substrate.get_chain_head() - ( - my_prev_coldkey_balance_, - delegate_owner, - my_prev_delegated_stake, - ) = await asyncio.gather( - subtensor.get_balance( - wallet.coldkey.ss58_address, block_hash=initial_block_hash - ), - get_hotkey_owner(delegate_ss58, block_hash_=initial_block_hash), - get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block_hash_=initial_block_hash, - ), - ) - - my_prev_coldkey_balance = my_prev_coldkey_balance_[wallet.coldkey.ss58_address] - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - if delegate_string == "delegate": - staking_balance = Balance.from_tao(my_prev_coldkey_balance.tao) - else: - # Unstake all - staking_balance = Balance.from_tao(my_prev_delegated_stake.tao) - else: - staking_balance = Balance.from_tao(amount) - - # Check enough balance to stake. - if delegate_string == "delegate" and staking_balance > my_prev_coldkey_balance: - err_console.print( - ":cross_mark: [red]Not enough balance to stake[/red]:\n" - f" [bold blue]current balance[/bold blue]:{my_prev_coldkey_balance}\n" - f" [bold red]amount staking[/bold red]: {staking_balance}\n" - f" [bold white]coldkey: {wallet.name}[/bold white]" - ) - return False - - if delegate_string == "undelegate" and ( - my_prev_delegated_stake is None or staking_balance > my_prev_delegated_stake - ): - err_console.print( - "\n:cross_mark: [red]Not enough balance to unstake[/red]:\n" - f" [bold blue]current stake[/bold blue]: {my_prev_delegated_stake}\n" - f" [bold red]amount unstaking[/bold red]: {staking_balance}\n" - f" [bold white]coldkey: {wallet.name}[bold white]\n\n" - ) - return False - - if delegate: - # Grab the existential deposit. - existential_deposit = await subtensor.get_existential_deposit() - - # Remove existential balance to keep key alive. - if staking_balance > my_prev_coldkey_balance - existential_deposit: - staking_balance = my_prev_coldkey_balance - existential_deposit - else: - staking_balance = staking_balance - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"\n[bold blue]Current stake[/bold blue]: [blue]{my_prev_delegated_stake}[/blue]\n" - f"[bold white]Do you want to {delegate_string}:[/bold white]\n" - f" [bold red]amount[/bold red]: [red]{staking_balance}\n[/red]" - f" [bold yellow]{'to' if delegate_string == 'delegate' else 'from'} hotkey[/bold yellow]: [yellow]{delegate_ss58}\n[/yellow]" - f" [bold green]hotkey owner[/bold green]: [green]{delegate_owner}[/green]" - ): - return False - - with console.status( - f":satellite: Staking to: [bold white]{subtensor}[/bold white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Transmitting delegate operation call") - staking_response, err_msg = await _do_delegation(staking_balance) - - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - console.print(":white_heavy_check_mark: [green]Finalized[/green]\n") - with console.status( - f":satellite: Checking Balance on: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching balance and stakes", status) - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_delegate_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkey.ss58_address, block_hash=block_hash - ), - get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block_hash_=block_hash, - ), - ) - - console.print( - "Balance:\n" - f" [blue]{my_prev_coldkey_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]\n" - "Stake:\n" - f" [blue]{my_prev_delegated_stake}[/blue] :arrow_right: [green]{new_delegate_stake}[/green]" - ) - return True - else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False - - -async def nominate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - """Becomes a delegate for the hotkey. - - :param wallet: The unlocked wallet to become a delegate for. - :param subtensor: The SubtensorInterface to use for the transaction - :param wait_for_finalization: Wait for finalization or not - :param wait_for_inclusion: Wait for inclusion or not - - :return: success - """ - with console.status( - ":satellite: Sending nominate call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="become_delegate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - - else: - err_console.print(f":cross_mark: [red]Failed[/red]: error:{err_msg}") - return success - - -# Commands - - -async def root_list(subtensor: SubtensorInterface): - """List the root network""" - - async def _get_list() -> tuple: - senate_query = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - ) - sm = [decode_account_id(i[x][0]) for i in senate_query for x in range(len(i))] - - rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) - if not rn: - return [], [], {}, {} - - di: dict[str, DelegatesDetails] = await subtensor.get_delegate_identities() - ts: dict[str, ScaleType] = await subtensor.substrate.query_multiple( - [n.hotkey for n in rn], - module="SubtensorModule", - storage_function="TotalHotkeyStake", - reuse_block_hash=True, - ) - return sm, rn, di, ts - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ): - senate_members, root_neurons, delegate_info, total_stakes = await _get_list() - total_tao = sum( - float(Balance.from_rao(total_stakes[neuron.hotkey])) - for neuron in root_neurons - ) - - table = Table( - Column( - "[bold white]UID", - style="dark_orange", - no_wrap=True, - footer=f"[bold]{len(root_neurons)}[/bold]", - ), - Column( - "[bold white]NAME", - style="bright_cyan", - no_wrap=True, - ), - Column( - "[bold white]ADDRESS", - style="bright_magenta", - no_wrap=True, - ), - Column( - "[bold white]STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - footer=f"{total_tao:.2f} (\u03c4) ", - ), - Column( - "[bold white]SENATOR", - style="dark_sea_green", - no_wrap=True, - ), - title=f"[underline dark_orange]Root Network[/underline dark_orange]\n[dark_orange]Network {subtensor.network}", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - leading=True, - ) - - if not root_neurons: - err_console.print( - f"[red]Error: No neurons detected on the network:[/red] [white]{subtensor}" - ) - raise typer.Exit() - - sorted_root_neurons = sorted( - root_neurons, - key=lambda neuron: float(Balance.from_rao(total_stakes[neuron.hotkey])), - reverse=True, - ) - - for neuron_data in sorted_root_neurons: - table.add_row( - str(neuron_data.uid), - ( - delegate_info[neuron_data.hotkey].display - if neuron_data.hotkey in delegate_info - else "~" - ), - neuron_data.hotkey, - "{:.5f}".format(float(Balance.from_rao(total_stakes[neuron_data.hotkey]))), - "Yes" if neuron_data.hotkey in senate_members else "No", - ) - - return console.print(table) - - -async def set_weights( - wallet: Wallet, - subtensor: SubtensorInterface, - netuids: list[int], - weights: list[float], - prompt: bool, -): - """Set weights for root network.""" - netuids_ = np.array(netuids, dtype=np.int64) - weights_ = np.array(weights, dtype=np.float32) - console.print(f"Setting weights in [dark_orange]network: {subtensor.network}") - - # Run the set weights operation. - - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=netuids_, - weights=weights_, - version_key=0, - prompt=prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - -async def get_weights( - subtensor: SubtensorInterface, - limit_min_col: Optional[int], - limit_max_col: Optional[int], - reuse_last: bool, - html_output: bool, - no_cache: bool, -): - """Get weights for root network.""" - if not reuse_last: - with console.status( - ":satellite: Fetching weights from chain...", spinner="aesthetic" - ): - weights = await subtensor.weights(0) - - uid_to_weights: dict[int, dict] = {} - netuids = set() - for matrix in weights: - [uid, weights_data] = matrix - - if not len(weights_data): - uid_to_weights[uid] = {} - normalized_weights = [] - else: - normalized_weights = np.array(weights_data)[:, 1] / max( - np.sum(weights_data, axis=0)[1], 1 - ) - - for weight_data, normalized_weight in zip(weights_data, normalized_weights): - [netuid, _] = weight_data - netuids.add(netuid) - if uid not in uid_to_weights: - uid_to_weights[uid] = {} - - uid_to_weights[uid][netuid] = normalized_weight - rows: list[list[str]] = [] - for uid in uid_to_weights: - row = [str(uid)] - - uid_weights = uid_to_weights[uid] - for netuid in netuids: - if netuid in uid_weights: - row.append("{:0.2f}%".format(uid_weights[netuid] * 100)) - else: - row.append("~") - rows.append(row) - - if not no_cache: - db_cols = [("UID", "INTEGER")] - for netuid in netuids: - db_cols.append((f"_{netuid}", "TEXT")) - create_table("rootgetweights", db_cols, rows) - netuids = list(netuids) - update_metadata_table( - "rootgetweights", - {"rows": json.dumps(rows), "netuids": json.dumps(netuids)}, - ) - else: - metadata = get_metadata_table("rootgetweights") - rows = json.loads(metadata["rows"]) - netuids = json.loads(metadata["netuids"]) - - _min_lim = limit_min_col if limit_min_col is not None else 0 - _max_lim = limit_max_col + 1 if limit_max_col is not None else len(netuids) - _max_lim = min(_max_lim, len(netuids)) - - if _min_lim is not None and _min_lim > len(netuids): - err_console.print("Minimum limit greater than number of netuids") - return - - if not html_output: - table = Table( - show_footer=True, - box=None, - pad_edge=False, - width=None, - title="[white]Root Network Weights", - ) - table.add_column( - "[white]UID", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - netuids = list(netuids) - for netuid in netuids[_min_lim:_max_lim]: - table.add_column( - f"[white]{netuid}", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - if not rows: - err_console.print("No weights exist on the root network.") - return - - # Adding rows - for row in rows: - new_row = [row[0]] + row[_min_lim + 1 : _max_lim + 1] - table.add_row(*new_row) - - return console.print(table) - - else: - html_cols = [{"title": "UID", "field": "UID"}] - for netuid in netuids[_min_lim:_max_lim]: - html_cols.append({"title": str(netuid), "field": f"_{netuid}"}) - render_table( - "rootgetweights", - "Root Network Weights", - html_cols, - ) - - -async def _get_my_weights( - subtensor: SubtensorInterface, ss58_address: str, my_uid: str -) -> NDArray[np.float32]: - """Retrieves the weight array for a given hotkey SS58 address.""" - - my_weights_, total_subnets_ = await asyncio.gather( - subtensor.substrate.query( - "SubtensorModule", "Weights", [0, my_uid], reuse_block_hash=True - ), - subtensor.substrate.query( - "SubtensorModule", "TotalNetworks", reuse_block_hash=True - ), - ) - # If setting weights for the first time, pass 0 root weights - my_weights: list[tuple[int, int]] = ( - my_weights_ if my_weights_ is not None else [(0, 0)] - ) - total_subnets: int = total_subnets_ - - print_verbose("Fetching current weights") - for _, w in enumerate(my_weights): - if w: - print_verbose(f"{w}") - - uids, values = zip(*my_weights) - weight_array = convert_weight_uids_and_vals_to_tensor(total_subnets, uids, values) - return weight_array - - -async def set_boost( - wallet: Wallet, - subtensor: SubtensorInterface, - netuid: int, - amount: float, - prompt: bool, -): - """Boosts weight of a given netuid for root network.""" - console.print(f"Boosting weights in [dark_orange]network: {subtensor.network}") - print_verbose(f"Fetching uid of hotkey on root: {wallet.hotkey_str}") - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] - ) - - if my_uid is None: - err_console.print("Your hotkey is not registered to the root network") - return False - - print_verbose("Fetching current weights") - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address, my_uid) - prev_weights = my_weights.copy() - my_weights[netuid] += amount - all_netuids = np.arange(len(my_weights)) - - console.print( - f"Boosting weight for netuid {netuid}\n\tfrom {prev_weights[netuid]} to {my_weights[netuid]}\n" - ) - console.print( - f"Previous weights -> Raw weights: \n\t{prev_weights} -> \n\t{my_weights}" - ) - - print_verbose(f"All netuids: {all_netuids}") - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def set_slash( - wallet: Wallet, - subtensor: SubtensorInterface, - netuid: int, - amount: float, - prompt: bool, -): - """Slashes weight""" - console.print(f"Slashing weights in [dark_orange]network: {subtensor.network}") - print_verbose(f"Fetching uid of hotkey on root: {wallet.hotkey_str}") - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] - ) - if my_uid is None: - err_console.print("Your hotkey is not registered to the root network") - return False - - print_verbose("Fetching current weights") - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address, my_uid) - prev_weights = my_weights.copy() - my_weights[netuid] -= amount - my_weights[my_weights < 0] = 0 # Ensure weights don't go negative - all_netuids = np.arange(len(my_weights)) - - console.print( - f"Slashing weight for netuid {netuid}\n\tfrom {prev_weights[netuid]} to {my_weights[netuid]}\n" - ) - console.print( - f"Previous weights -> Raw weights: \n\t{prev_weights} -> \n\t{my_weights}" - ) - - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def senate_vote( - wallet: Wallet, - subtensor: SubtensorInterface, - proposal_hash: str, - vote: bool, - prompt: bool, -) -> bool: - """Vote in Bittensor's governance protocol proposals""" - - if not proposal_hash: - err_console.print( - "Aborting: Proposal hash not specified. View all proposals with the `proposals` command." - ) - return False - elif not _validate_proposal_hash(proposal_hash): - err_console.print( - "Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long" - ) - return False - - print_verbose(f"Fetching senate status of {wallet.hotkey_str}") - if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." - ) - return False - - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") - vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True) - if not vote_data: - err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return False - - success = await vote_senate_extrinsic( - subtensor=subtensor, - wallet=wallet, - proposal_hash=proposal_hash, - proposal_idx=vote_data.index, - vote=vote, - wait_for_inclusion=True, - wait_for_finalization=False, - prompt=prompt, - ) - - return success - - -async def get_senate(subtensor: SubtensorInterface): - """View Bittensor's governance protocol proposals""" - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching senate members", status) - senate_members = await _get_senate_members(subtensor) - - print_verbose("Fetching member details from Github") - delegate_info: dict[ - str, DelegatesDetails - ] = await subtensor.get_delegate_identities() - - table = Table( - Column( - "[bold white]NAME", - style="bright_cyan", - no_wrap=True, - ), - Column( - "[bold white]ADDRESS", - style="bright_magenta", - no_wrap=True, - ), - title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - leading=True, - ) - - for ss58_address in senate_members: - table.add_row( - ( - delegate_info[ss58_address].display - if ss58_address in delegate_info - else "~" - ), - ss58_address, - ) - - return console.print(table) - - -async def register(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): - """Register neuron by recycling some TAO.""" - - console.print( - f"Registering on [dark_orange]netuid 0[/dark_orange] on network: [dark_orange]{subtensor.network}" - ) - - # Check current recycle amount - print_verbose("Fetching recycle amount & balance") - recycle_call, balance_ = await asyncio.gather( - subtensor.get_hyperparameter(param_name="Burn", netuid=0, reuse_block=True), - subtensor.get_balance(wallet.coldkeypub.ss58_address, reuse_block=True), - ) - current_recycle = Balance.from_rao(int(recycle_call)) - try: - balance: Balance = balance_[wallet.coldkeypub.ss58_address] - except TypeError as e: - err_console.print(f"Unable to retrieve current recycle. {e}") - return False - except KeyError: - err_console.print("Unable to retrieve current balance.") - return False - - # Check balance is sufficient - if balance < current_recycle: - err_console.print( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]" - ) - return False - - if prompt: - if not Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\n" - f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" - f"Do you want to continue?", - default=False, - ): - return False - - await root_register_extrinsic( - subtensor, - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def proposals(subtensor: SubtensorInterface): - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - print_verbose("Fetching senate members & proposals") - block_hash = await subtensor.substrate.get_chain_head() - senate_members, all_proposals = await asyncio.gather( - _get_senate_members(subtensor, block_hash), - _get_proposals(subtensor, block_hash), - ) - - print_verbose("Fetching member information from Chain") - registered_delegate_info: dict[ - str, DelegatesDetails - ] = await subtensor.get_delegate_identities() - - table = Table( - Column( - "[white]HASH", - style="light_goldenrod2", - no_wrap=True, - ), - Column("[white]THRESHOLD", style="rgb(42,161,152)"), - Column("[white]AYES", style="green"), - Column("[white]NAYS", style="red"), - Column( - "[white]VOTES", - style="rgb(50,163,219)", - ), - Column("[white]END", style="bright_cyan"), - Column("[white]CALLDATA", style="dark_sea_green"), - title=f"\n[dark_orange]Proposals\t\t\nActive Proposals: {len(all_proposals)}\t\tSenate Size: {len(senate_members)}\nNetwork: {subtensor.network}", - show_footer=True, - box=box.SIMPLE_HEAVY, - pad_edge=False, - width=None, - border_style="bright_black", - ) - for hash_, (call_data, vote_data) in all_proposals.items(): - table.add_row( - hash_, - str(vote_data.threshold), - str(len(vote_data.ayes)), - str(len(vote_data.nays)), - display_votes(vote_data, registered_delegate_info), - str(vote_data.end), - format_call_data(call_data), - ) - return console.print(table) - - -async def set_take(wallet: Wallet, subtensor: SubtensorInterface, take: float) -> bool: - """Set delegate take.""" - - async def _do_set_take() -> bool: - """ - Just more easily allows an early return and to close the substrate interface after the logic - """ - print_verbose("Checking if hotkey is a delegate") - # Check if the hotkey is not a delegate. - if not await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} is NOT a delegate." - ) - return False - - if take > 0.18 or take < 0: - err_console.print("ERROR: Take value should not exceed 18% or be below 0%") - return False - - result: bool = await set_take_extrinsic( - subtensor=subtensor, - wallet=wallet, - delegate_ss58=wallet.hotkey.ss58_address, - take=take, - ) - - if not result: - err_console.print("Could not set the take") - return False - else: - # Check if we are a delegate. - is_delegate: bool = await subtensor.is_hotkey_delegate( - wallet.hotkey.ss58_address - ) - if not is_delegate: - err_console.print( - "Could not set the take [white]{}[/white]".format(subtensor.network) - ) - return False - else: - console.print( - "Successfully set the take on [white]{}[/white]".format( - subtensor.network - ) - ) - return True - - console.print(f"Setting take on [dark_orange]network: {subtensor.network}") - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - result_ = await _do_set_take() - - return result_ - - -async def delegate_stake( - wallet: Wallet, - subtensor: SubtensorInterface, - amount: Optional[float], - delegate_ss58key: str, - prompt: bool, -): - """Delegates stake to a chain delegate.""" - console.print(f"Delegating stake on [dark_orange]network: {subtensor.network}") - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - amount, - wait_for_inclusion=True, - prompt=prompt, - delegate=True, - ) - - -async def delegate_unstake( - wallet: Wallet, - subtensor: SubtensorInterface, - amount: Optional[float], - delegate_ss58key: str, - prompt: bool, -): - """Undelegates stake from a chain delegate.""" - console.print(f"Undelegating stake on [dark_orange]network: {subtensor.network}") - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - amount, - wait_for_inclusion=True, - prompt=prompt, - delegate=False, - ) - - -async def my_delegates( - wallet: Wallet, subtensor: SubtensorInterface, all_wallets: bool -): - """Delegates stake to a chain delegate.""" - - async def wallet_to_delegates( - w: Wallet, bh: str - ) -> tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]]: - """Helper function to retrieve the validity of the wallet (if it has a coldkeypub on the device) - and its delegate info.""" - if not w.coldkeypub_file.exists_on_device(): - return None, None - else: - delegates_ = await subtensor.get_delegated( - w.coldkeypub.ss58_address, block_hash=bh - ) - return w, delegates_ - - wallets = get_coldkey_wallets_for_path(wallet.path) if all_wallets else [wallet] - - table = Table( - Column("[white]Wallet", style="bright_cyan"), - Column( - "[white]OWNER", - style="bold bright_cyan", - overflow="fold", - justify="left", - ratio=1, - ), - Column( - "[white]SS58", - style="bright_magenta", - justify="left", - overflow="fold", - ratio=3, - ), - Column("[white]Delegation", style="dark_orange", no_wrap=True, ratio=1), - Column("[white]\u03c4/24h", style="bold green", ratio=1), - Column( - "[white]NOMS", - justify="center", - style="rgb(42,161,152)", - no_wrap=True, - ratio=1, - ), - Column( - "[white]OWNER STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column( - "[white]TOTAL STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column("[white]SUBNETS", justify="right", style="white", ratio=1), - Column("[white]VPERMIT", justify="right"), - Column( - "[white]24h/k\u03c4", style="rgb(42,161,152)", justify="center", ratio=1 - ), - Column("[white]Desc", style="rgb(50,163,219)", ratio=3), - title=f"[underline dark_orange]My Delegates[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - box=box.SIMPLE_HEAVY, - border_style="bright_black", - leading=True, - ) - - total_delegated = 0 - - # TODO: this doesnt work when passed to wallets_with_delegates - # block_hash = await subtensor.substrate.get_chain_head() - - registered_delegate_info: dict[str, DelegatesDetails] - wallets_with_delegates: tuple[ - tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]] - ] - - print_verbose("Fetching delegate information") - wallets_with_delegates, registered_delegate_info = await asyncio.gather( - asyncio.gather(*[wallet_to_delegates(wallet_, None) for wallet_ in wallets]), - subtensor.get_delegate_identities(), - ) - if not registered_delegate_info: - console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - - print_verbose("Processing delegate information") - for wall, delegates in wallets_with_delegates: - if not wall or not delegates: - continue - - my_delegates_ = {} # hotkey, amount - for delegate in delegates: - for coldkey_addr, staked in delegate[0].nominators: - if coldkey_addr == wall.coldkeypub.ss58_address and staked.tao > 0: - my_delegates_[delegate[0].hotkey_ss58] = staked - - delegates.sort(key=lambda d: d[0].total_stake, reverse=True) - total_delegated += sum(my_delegates_.values()) - - for i, delegate in enumerate(delegates): - owner_stake = next( - ( - stake - for owner, stake in delegate[0].nominators - if owner == delegate[0].owner_ss58 - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate[0].hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].display - delegate_url = registered_delegate_info[delegate[0].hotkey_ss58].web - delegate_description = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].additional - else: - delegate_name = "~" - delegate_url = "" - delegate_description = "" - - if delegate[0].hotkey_ss58 in my_delegates_: - twenty_four_hour = delegate[0].total_daily_return.tao * ( - my_delegates_[delegate[0].hotkey_ss58] / delegate[0].total_stake.tao - ) - table.add_row( - wall.name, - Text(delegate_name, style=f"link {delegate_url}"), - f"{delegate[0].hotkey_ss58}", - f"{my_delegates_[delegate[0].hotkey_ss58]!s:13.13}", - f"{twenty_four_hour!s:6.6}", - str(len(delegate[0].nominators)), - f"{owner_stake!s:13.13}", - f"{delegate[0].total_stake!s:13.13}", - group_subnets(delegate[0].registrations), - group_subnets(delegate[0].validator_permits), - f"{delegate[0].total_daily_return.tao * (1000 / (0.001 + delegate[0].total_stake.tao))!s:6.6}", - str(delegate_description), - ) - if console.width < 150: - console.print( - "[yellow]Warning: Your terminal width might be too small to view all the information clearly" - ) - console.print(table) - console.print(f"Total delegated TAO: {total_delegated}") - - -async def list_delegates(subtensor: SubtensorInterface): - """List all delegates on the network.""" - - with console.status( - ":satellite: Loading delegates...", spinner="aesthetic" - ) as status: - print_verbose("Fetching delegate details from chain", status) - block_hash = await subtensor.substrate.get_chain_head() - registered_delegate_info, block_number, delegates = await asyncio.gather( - subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.substrate.get_block_number(block_hash), - subtensor.get_delegates(block_hash=block_hash), - ) - - print_verbose("Fetching previous delegates info from chain", status) - - async def get_prev_delegates(fallback_offsets=(1200, 200)): - for offset in fallback_offsets: - try: - prev_block_hash = await subtensor.substrate.get_block_hash( - max(0, block_number - offset) - ) - return await subtensor.get_delegates(block_hash=prev_block_hash) - except SubstrateRequestException: - continue - return None - - prev_delegates = await get_prev_delegates() - - if prev_delegates is None: - err_console.print( - ":warning: [yellow]Could not fetch delegates history. [/yellow]" - ) - - delegates.sort(key=lambda d: d.total_stake, reverse=True) - prev_delegates_dict = {} - if prev_delegates is not None: - for prev_delegate in prev_delegates: - prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - - if not registered_delegate_info: - console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - table = Table( - Column( - "[white]INDEX\n\n", - str(len(delegates)), - style="bold white", - ), - Column( - "[white]DELEGATE\n\n", - style="bold bright_cyan", - justify="left", - overflow="fold", - ratio=1, - ), - Column( - "[white]SS58\n\n", - style="bright_magenta", - no_wrap=False, - overflow="fold", - ratio=2, - ), - Column( - "[white]NOMINATORS\n\n", - justify="center", - style="gold1", - no_wrap=True, - ratio=1, - ), - Column( - "[white]OWN STAKE\n(\u03c4)\n", - justify="right", - style="orange1", - no_wrap=True, - ratio=1, - ), - Column( - "[white]TOTAL STAKE\n(\u03c4)\n", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column("[white]CHANGE\n/(4h)\n", style="grey0", justify="center", ratio=1), - Column("[white]TAKE\n\n", style="white", no_wrap=True, ratio=1), - Column( - "[white]NOMINATOR\n/(24h)/k\u03c4\n", - style="dark_olive_green3", - justify="center", - ratio=1, - ), - Column( - "[white]DELEGATE\n/(24h)\n", - style="dark_olive_green3", - justify="center", - ratio=1, - ), - Column( - "[white]VPERMIT\n\n", - justify="center", - no_wrap=False, - max_width=20, - style="dark_sea_green", - ratio=2, - ), - Column("[white]Desc\n\n", style="rgb(50,163,219)", max_width=30, ratio=2), - title=f"[underline dark_orange]Root Delegates[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - pad_edge=False, - box=None, - ) - - for i, delegate in enumerate(delegates): - owner_stake = next( - ( - stake - for owner, stake in delegate.nominators - if owner == delegate.owner_ss58 - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[delegate.hotkey_ss58].display - delegate_url = registered_delegate_info[delegate.hotkey_ss58].web - delegate_description = registered_delegate_info[ - delegate.hotkey_ss58 - ].additional - else: - delegate_name = "~" - delegate_url = "" - delegate_description = "" - - if delegate.hotkey_ss58 in prev_delegates_dict: - prev_stake = prev_delegates_dict[delegate.hotkey_ss58].total_stake - if prev_stake == 0: - if delegate.total_stake > 0: - rate_change_in_stake_str = "[green]100%[/green]" - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake = ( - 100 - * (float(delegate.total_stake) - float(prev_stake)) - / float(prev_stake) - ) - if rate_change_in_stake > 0: - rate_change_in_stake_str = "[green]{:.2f}%[/green]".format( - rate_change_in_stake - ) - elif rate_change_in_stake < 0: - rate_change_in_stake_str = "[red]{:.2f}%[/red]".format( - rate_change_in_stake - ) - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake_str = "[grey0]NA[/grey0]" - table.add_row( - # INDEX - str(i), - # DELEGATE - Text(delegate_name, style=f"link {delegate_url}"), - # SS58 - f"{delegate.hotkey_ss58}", - # NOMINATORS - str(len([nom for nom in delegate.nominators if nom[1].rao > 0])), - # DELEGATE STAKE - f"{owner_stake!s:13.13}", - # TOTAL STAKE - f"{delegate.total_stake!s:13.13}", - # CHANGE/(4h) - rate_change_in_stake_str, - # TAKE - f"{delegate.take * 100:.1f}%", - # NOMINATOR/(24h)/k - f"{Balance.from_tao(delegate.total_daily_return.tao * (1000 / (0.001 + delegate.total_stake.tao)))!s:6.6}", - # DELEGATE/(24h) - f"{Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", - # VPERMIT - str(group_subnets(delegate.registrations)), - # Desc - str(delegate_description), - end_section=True, - ) - console.print(table) - - -async def nominate(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): - """Nominate wallet.""" - - console.print(f"Nominating on [dark_orange]network: {subtensor.network}") - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - print_verbose(f"Checking hotkey ({wallet.hotkey_str}) is a delegate") - # Check if the hotkey is already a delegate. - if await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} is already a delegate." - ) - return - - print_verbose("Nominating hotkey as a delegate") - result: bool = await nominate_extrinsic(subtensor, wallet) - if not result: - err_console.print( - f"Could not became a delegate on [white]{subtensor.network}[/white]" - ) - return - else: - # Check if we are a delegate. - print_verbose("Confirming delegate status") - is_delegate: bool = await subtensor.is_hotkey_delegate( - wallet.hotkey.ss58_address - ) - if not is_delegate: - err_console.print( - f"Could not became a delegate on [white]{subtensor.network}[/white]" - ) - return - console.print( - f"Successfully became a delegate on [white]{subtensor.network}[/white]" - ) - - # Prompt use to set identity on chain. - if prompt: - do_set_identity = Confirm.ask("Would you like to set your identity? [y/n]") - - if do_set_identity: - id_prompts = set_id_prompts(validator=True) - await set_id(wallet, subtensor, *id_prompts, prompt=prompt) From 1215d67fc18488b9b3b01edee2122aa44abab866 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 17 Oct 2024 17:29:24 +0200 Subject: [PATCH 025/332] subnets list --- bittensor_cli/cli.py | 28 +-- bittensor_cli/src/commands/subnets.py | 294 +++++++++----------------- 2 files changed, 115 insertions(+), 207 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8566a180..11e72806 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3034,8 +3034,8 @@ def sudo_get( def subnets_list( self, network: Optional[list[str]] = Options.network, - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, + # reuse_last: bool = Options.reuse_last, + # html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -3058,21 +3058,21 @@ def subnets_list( [green]$[/green] btcli subnets list """ self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: - err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " - "Change the config to 'False' using `btcli config set`." - ) - raise typer.Exit() - if reuse_last: - subtensor = None - else: - subtensor = self.initialize_chain(network) + # if (reuse_last or html_output) and self.config.get("use_cache") is False: + # err_console.print( + # "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " + # "Change the config to 'False' using `btcli config set`." + # ) + # raise typer.Exit() + # if reuse_last: + # subtensor = None + # else: + subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( subtensor, - reuse_last, - html_output, + False, # reuse-last + False, # html-output not self.config.get("use_cache", True), ) ) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 3e934982..93e6390d 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -153,204 +153,112 @@ async def subnets_list( subtensor: "SubtensorInterface", reuse_last: bool, html_output: bool, no_cache: bool ): """List all subnet netuids in the network.""" - - async def _get_all_subnets_info(): - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", method="get_subnets_info", params=[] - ) - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return SubnetInfo.list_from_vec_u8(bytes_result) - - if not reuse_last: - subnets: list[SubnetInfo] - delegate_info: dict[str, DelegatesDetails] - - print_verbose("Fetching subnet and delegate information") - subnets, delegate_info = await asyncio.gather( - _get_all_subnets_info(), - subtensor.get_delegate_identities(), - ) - - if not subnets: - err_console.print("[red]No subnets found[/red]") - return - - rows = [] - db_rows = [] - total_neurons = 0 - max_neurons = 0 - - for subnet in subnets: - total_neurons += subnet.subnetwork_n - max_neurons += subnet.max_n - rows.append( - ( - str(subnet.netuid), - str(subnet.subnetwork_n), - str(millify(subnet.max_n)), - f"{subnet.emission_value / RAO_PER_TAO * 100:0.2f}%", - str(subnet.tempo), - f"{subnet.burn!s:8.8}", - str(millify(subnet.difficulty)), - str( - delegate_info[subnet.owner_ss58].display - if subnet.owner_ss58 in delegate_info - else subnet.owner_ss58 - ), - ) - ) - db_rows.append( - [ - int(subnet.netuid), - int(subnet.subnetwork_n), - int(subnet.max_n), # millified in HTML table - float( - subnet.emission_value / RAO_PER_TAO * 100 - ), # shown as percentage in HTML table - int(subnet.tempo), - float(subnet.burn), - int(subnet.difficulty), # millified in HTML table - str( - delegate_info[subnet.owner_ss58].display - if subnet.owner_ss58 in delegate_info - else subnet.owner_ss58 - ), - ] + # TODO add reuse-last and html-output and no-cache + + # Initialize variables to store aggregated data + rows = [] + subnets = await subtensor.get_all_subnet_dynamic_info() + for subnet in subnets: + symbol = f"{subnet.symbol}\u200e" + rows.append( + ( + str(subnet.netuid), + f"[light_goldenrod1]{subnet.symbol}[light_goldenrod1]", + f"τ {subnet.emission.tao:.4f}", + # f"P( τ {subnet.tao_in.tao:,.4f},", + f"τ {subnet.tao_in.tao:,.4f}", + # f"{subnet.alpha_in.tao:,.4f} {subnet.symbol} )", + f"{subnet.alpha_out.tao:,.4f} {symbol}", + f"{subnet.price.tao:.4f} τ/{symbol}", + str(subnet.blocks_since_last_step) + "/" + str(subnet.tempo), + # f"{subnet.owner_locked}" + "/" + f"{subnet.total_locked}", + # f"{subnet.owner[:3]}...{subnet.owner[-3:]}", ) - metadata = { - "network": subtensor.network, - "netuid_count": len(subnets), - "N": total_neurons, - "MAX_N": max_neurons, - "rows": json.dumps(rows), - } - if not no_cache: - create_table( - "subnetslist", - [ - ("NETUID", "INTEGER"), - ("N", "INTEGER"), - ("MAX_N", "BLOB"), - ("EMISSION", "REAL"), - ("TEMPO", "INTEGER"), - ("RECYCLE", "REAL"), - ("DIFFICULTY", "BLOB"), - ("SUDO", "TEXT"), - ], - db_rows, - ) - update_metadata_table("subnetslist", values=metadata) - else: - try: - metadata = get_metadata_table("subnetslist") - rows = json.loads(metadata["rows"]) - except sqlite3.OperationalError: - err_console.print( - "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " - "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " - "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " - "issue." - ) - return - if not html_output: - table = Table( - title=f"[underline dark_orange]Subnets[/underline dark_orange]\n[dark_orange]Network: {metadata['network']}[/dark_orange]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) - table.add_column( - "[bold white]NETUID", - footer=f"[white]{metadata['netuid_count']}[/white]", - style="white", - justify="center", - ) - table.add_column( - "[bold white]N", - footer=f"[white]{metadata['N']}[/white]", - style="bright_cyan", - justify="right", - ) - table.add_column( - "[bold white]MAX_N", - footer=f"[white]{metadata['MAX_N']}[/white]", - style="bright_cyan", - justify="right", - ) - table.add_column( - "[bold white]EMISSION", style="light_goldenrod2", justify="right" - ) - table.add_column("[bold white]TEMPO", style="rgb(42,161,152)", justify="right") - table.add_column("[bold white]RECYCLE", style="light_salmon3", justify="right") - table.add_column("[bold white]POW", style="medium_purple", justify="right") - table.add_column( - "[bold white]SUDO", style="bright_magenta", justify="right", overflow="fold" - ) + # Define table properties + console_width = console.width - 5 + table = Table( + title="Subnet Info", + width=console_width, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + table.title = f"[white]Subnets - {subtensor.network}\n" + + # Add columns to the table + # price_total = f"τ{total_price.tao:.2f}/{bt.Balance.from_rao(dynamic_emission).tao:.2f}" + # above_price_threshold = total_price.tao > bt.Balance.from_rao(dynamic_emission).tao + + table.add_column("Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center") + table.add_column("Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center") + table.add_column( + f"Emission ({Balance.get_unit(0)})", + style="rgb(38,139,210)", + no_wrap=True, + justify="right", + ) + table.add_column( + f"TAO({Balance.get_unit(0)})", + style="medium_purple", + no_wrap=True, + justify="right", + ) + # table.add_column(f"{bt.Balance.get_unit(1)})", style="rgb(42,161,152)", no_wrap=True, justify="left") + table.add_column( + f"Stake({Balance.get_unit(1)})", style="green", no_wrap=True, justify="right" + ) + table.add_column( + f"Rate ({Balance.get_unit(1)}/{Balance.get_unit(0)})", + style="light_goldenrod2", + no_wrap=True, + justify="center", + ) + table.add_column( + "Tempo (k/n)", style="light_salmon3", no_wrap=True, justify="center" + ) + # table.add_column(f"Locked ({bt.Balance.get_unit(1)})", style="rgb(38,139,210)", no_wrap=True, justify="center") + # table.add_column("Owner", style="rgb(38,139,210)", no_wrap=True, justify="center") - for row in rows: - table.add_row(*row) + # Sort rows by subnet.emission.tao, keeping the first subnet in the first position + sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) - console.print(table) - console.print( - dedent( - """ - Description: - The table displays the list of subnets registered in the Bittensor network. - - NETUID: The network identifier of the subnet. - - N: The current UIDs registered to the network. - - MAX_N: The total UIDs allowed on the network. - - EMISSION: The emission accrued by this subnet in the network. - - TEMPO: A duration of a number of blocks. Several subnet events occur at the end of every tempo period. - - RECYCLE: Cost to register to the subnet. - - POW: Proof of work metric of the subnet. - - SUDO: Owner's identity. - """ - ) - ) - else: - render_table( - "subnetslist", - f"Subnets List | Network: {metadata['network']} - " - f"Netuids: {metadata['netuid_count']} - N: {metadata['N']}", - columns=[ - {"title": "NetUID", "field": "NETUID"}, - {"title": "N", "field": "N"}, - {"title": "MAX_N", "field": "MAX_N", "customFormatter": "millify"}, - { - "title": "EMISSION", - "field": "EMISSION", - "formatter": "money", - "formatterParams": { - "symbolAfter": "p", - "symbol": "%", - "precision": 2, - }, - }, - {"title": "Tempo", "field": "TEMPO"}, - { - "title": "Recycle", - "field": "RECYCLE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Difficulty", - "field": "DIFFICULTY", - "customFormatter": "millify", - }, - {"title": "sudo", "field": "SUDO"}, - ], - ) + # Add rows to the table + for row in sorted_rows: + table.add_row(*row) + + # Print the table + console.print(table) + console.print( + """ +[bold white]Description[/bold white]: +The table displays relevant information about each subnet on the network. +The columns are as follows: + - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). + - [bold white]Symbol[/bold white]: The symbol representing the subnet's stake. + - [bold white]Emission[/bold white]: The amount of TAO added to the subnet every block. Calculated by dividing the TAO (t) column values by the sum of the TAO (t) column. + - [bold white]TAO[/bold white]: The TAO staked into the subnet ( which dynamically changes during stake, unstake and emission events ). + - [bold white]Stake[/bold white]: The outstanding supply of stake across all staking accounts on this subnet. + - [bold white]Rate[/bold white]: The rate of conversion between TAO and the subnet's staking unit. + - [bold white]Tempo[/bold white]: The number of blocks between epochs. Represented as (k/n) where k is the blocks since the last epoch and n is the total blocks in the epoch. +""" + ) async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: From bc0be72da7083b4b87f46972b8a6c262e5924aa5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 18 Oct 2024 10:49:31 +0200 Subject: [PATCH 026/332] subnets register --- .../src/bittensor/extrinsics/registration.py | 112 ++++++++++++++++++ bittensor_cli/src/commands/subnets.py | 60 +++++++++- 2 files changed, 169 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index af696210..d1a0629c 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -29,6 +29,7 @@ from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src.bittensor.chain_data import NeuronInfo +from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -674,6 +675,117 @@ async def get_neuron_for_pubkey_and_subnet(): return False +async def burned_register_extrinsic( + subtensor: SubtensorInterface, + wallet: Wallet, + netuid: int, + old_balance: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + prompt: bool = False, +) -> bool: + """Registers the wallet to chain by recycling TAO. + + :param subtensor: The SubtensorInterface object to use for the call, initialized + :param wallet: Bittensor wallet object. + :param netuid: The `netuid` of the subnet to register on. + :param old_balance: The wallet balance prior to the registration burn. + :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, + or returns `False` if the extrinsic fails to be finalized within the timeout. + :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + + :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status( + f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", + spinner="aesthetic", + ) as status: + my_uid = await subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ) + + print_verbose("Checking if already registered", status) + neuron = await subtensor.neuron_for_uid( + uid=my_uid, + netuid=netuid, + block_hash=subtensor.substrate.last_block_hash, + ) + + if not neuron.is_null: + console.print( + ":white_heavy_check_mark: [green]Already Registered[/green]:\n" + f"uid: [bold white]{neuron.uid}[/bold white]\n" + f"netuid: [bold white]{neuron.netuid}[/bold white]\n" + f"hotkey: [bold white]{neuron.hotkey}[/bold white]\n" + f"coldkey: [bold white]{neuron.coldkey}[/bold white]" + ) + return True + + with console.status( + ":satellite: Recycling TAO for Registration...", spinner="aesthetic" + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + await asyncio.sleep(0.5) + return False + # Successful registration, final check for neuron and pubkey + else: + with console.status(":satellite: Checking Balance...", spinner="aesthetic"): + block_hash = await subtensor.substrate.get_chain_head() + new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + reuse_block=False, + ), + subtensor.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, block_hash=block_hash + ), + subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ), + ) + + console.print( + "Balance:\n" + f" [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" + ) + + if len(netuids_for_hotkey) > 0: + console.print( + f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" + ) + return True + else: + # neuron not found, try again + err_console.print( + ":cross_mark: [red]Unknown error. Neuron not found.[/red]" + ) + return False + + async def run_faucet_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 93e6390d..189f0dbb 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -12,9 +12,11 @@ from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetInfo -from bittensor_cli.src.bittensor.extrinsics.registration import register_extrinsic +from bittensor_cli.src.bittensor.extrinsics.registration import ( + register_extrinsic, + burned_register_extrinsic, +) from bittensor_cli.src.bittensor.minigraph import MiniGraph -from bittensor_cli.src.commands.root import burned_register_extrinsic from bittensor_cli.src.commands.wallets import set_id, set_id_prompts from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, @@ -176,6 +178,7 @@ async def subnets_list( ) ) + # TODO make this a reusable function # Define table properties console_width = console.width - 5 table = Table( @@ -359,6 +362,58 @@ async def register( return if prompt: + # TODO make this a reusable function, also used in subnets list + # Show creation table. + console_width = console.width - 5 + table = Table( + title="Subnet Info", + width=console_width, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + table.title = f"[white]Register - {subtensor.network}\n" + table.add_column( + "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" + ) + table.add_column( + "Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center" + ) + table.add_column( + f"Cost ({Balance.get_unit(0)})", + style="rgb(38,139,210)", + no_wrap=True, + justify="right", + ) + table.add_column( + "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + ) + table.add_column( + "Coldkey", style="bold dark_green", no_wrap=True, justify="center" + ) + table.add_row( + str(netuid), + f"[light_goldenrod1]{Balance.get_unit(netuid)}[light_goldenrod1]", + f"τ {current_recycle.tao:.4f}", + f"{wallet.hotkey.ss58_address}", + f"{wallet.coldkeypub.ss58_address}", + ) + console.print(table) if not ( Confirm.ask( f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is " @@ -373,7 +428,6 @@ async def register( wallet=wallet, netuid=netuid, prompt=False, - recycle_amount=current_recycle, old_balance=balance, ) From ec0cfd1b0560b9bb45a0b91a0456dd930d79eb4f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 18 Oct 2024 13:06:06 +0200 Subject: [PATCH 027/332] Subnets show --- bittensor_cli/cli.py | 18 ++ bittensor_cli/src/commands/subnets.py | 321 +++++++++++++++++++++++++- 2 files changed, 337 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 11e72806..aa798d19 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3077,6 +3077,24 @@ def subnets_list( ) ) + def subnets_show( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + prompt: bool = Options.prompt, + ): + self.verbosity_handler(quiet, verbose) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.show( + subtensor, + netuid, + prompt=prompt, + ) + ) + def subnets_lock_cost( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 189f0dbb..1e7cda68 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -9,9 +9,8 @@ from rich.prompt import Confirm from rich.table import Column, Table -from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import SubnetInfo +from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, @@ -264,6 +263,324 @@ async def subnets_list( ) +async def show(subtensor: "SubtensorInterface", netuid: int, prompt: bool = True): + async def show_root(): + all_subnets = await subtensor.get_all_subnet_dynamic_info() + root_state: "SubnetState" = SubnetState.from_vec_u8( + ( + await subtensor.substrate.rpc_request( + method="subnetInfo_getSubnetState", params=[0, None] + ) + )["result"] + ) + if root_state is None: + err_console.print("The root subnet does not exist") + return + if len(root_state.hotkeys) == 0: + err_console.print( + "The root-subnet is currently empty with 0 UIDs registered." + ) + return + + console_width = console.width - 5 + table = Table( + title="[white]Root Network", + width=console_width, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + # Add columns to the table + table.add_column( + "Position", style="rgb(253,246,227)", no_wrap=True, justify="center" + ) + table.add_column( + f"TAO ({Balance.get_unit(0)})", + style="medium_purple", + no_wrap=True, + justify="center", + ) + table.add_column( + f"Stake ({Balance.get_unit(0)})", + style="dark_sea_green", + no_wrap=True, + justify="center", + ) + table.add_column( + f"Emission ({Balance.get_unit(0)}/block)", + style="rgb(42,161,152)", + no_wrap=True, + justify="center", + ) + table.add_column( + "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + ) + sorted_hotkeys = sorted( + enumerate(root_state.hotkeys), + key=lambda x: root_state.global_stake[x[0]], + reverse=True, + ) + for pos, (idx, hk) in enumerate(sorted_hotkeys): + total_emission_per_block = 0 + for netuid_ in range(len(all_subnets)): + subnet = all_subnets[netuid_] + emission_on_subnet = ( + root_state.emission_history[netuid_][idx] / subnet.tempo + ) + total_emission_per_block += subnet.alpha_to_tao( + Balance.from_rao(emission_on_subnet) + ) + table.add_row( + str((pos + 1)), + str(root_state.global_stake[idx]), + str(root_state.local_stake[idx]), + str(total_emission_per_block), + f"{root_state.hotkeys[idx]}", + ) + + # Print the table + console.print(table) + console.print( + """ + Description: + The table displays the root subnet participants and their metrics. + The columns are as follows: + - Position: The sorted position of the hotkey by total TAO. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on root (measured in TAO). + - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. + - Hotkey: The hotkey ss58 address. + """ + ) + + async def show_subnet(netuid_: int): + subnet_info = await subtensor.get_subnet_dynamic_info(netuid_) + subnet_state: "SubnetState" = SubnetState.from_vec_u8( + ( + await subtensor.substrate.rpc_request( + method="subnetInfo_getSubnetState", params=[netuid_, None] + ) + )["result"] + ) + if subnet_info is None: + err_console.print(f"Subnet {netuid_} does not exist") + return + elif len(subnet_state.hotkeys) == 0: + err_console.print( + f"Subnet {netuid_} is currently empty with 0 UIDs registered." + ) + return + + # Define table properties + console_width = console.width - 5 + table = Table( + title=f"[white]Subnet {netuid_} Metagraph", + width=console_width, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + subnet_info_table = Table( + width=console_width, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=False, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + + subnet_info_table.add_column( + "Index", style="grey89", no_wrap=True, justify="center" + ) + subnet_info_table.add_column( + "Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center" + ) + subnet_info_table.add_column( + f"Emission ({Balance.get_unit(0)})", + style="rgb(38,139,210)", + no_wrap=True, + justify="center", + ) + subnet_info_table.add_column( + f"P({Balance.get_unit(0)},", + style="rgb(108,113,196)", + no_wrap=True, + justify="right", + ) + subnet_info_table.add_column( + f"{Balance.get_unit(1)})", + style="rgb(42,161,152)", + no_wrap=True, + justify="left", + ) + subnet_info_table.add_column( + f"{Balance.get_unit(1)}", + style="rgb(133,153,0)", + no_wrap=True, + justify="center", + ) + subnet_info_table.add_column( + f"Rate ({Balance.get_unit(1)}/{Balance.get_unit(0)})", + style="rgb(181,137,0)", + no_wrap=True, + justify="center", + ) + subnet_info_table.add_column( + "Tempo", style="rgb(38,139,210)", no_wrap=True, justify="center" + ) + subnet_info_table.add_row( + str(netuid_), + f"[light_goldenrod1]{str(subnet_info.symbol)}[light_goldenrod1]", + f"τ{subnet_info.emission.tao:.4f}", + f"P( τ{subnet_info.tao_in.tao:,.4f},", + f"{subnet_info.alpha_in.tao:,.4f}{subnet_info.symbol} )", + f"{subnet_info.alpha_out.tao:,.4f}{subnet_info.symbol}", + f"{subnet_info.price.tao:.4f}τ/{subnet_info.symbol}", + str(subnet_info.blocks_since_last_step) + "/" + str(subnet_info.tempo), + ) + + rows = [] + emission_sum = sum( + [ + subnet_state.emission[idx].tao + for idx in range(len(subnet_state.emission)) + ] + ) + for idx, hk in enumerate(subnet_state.hotkeys): + hotkey_block_emission = ( + subnet_state.emission[idx].tao / emission_sum + if emission_sum != 0 + else 0 + ) + rows.append( + ( + str(idx), + str(subnet_state.global_stake[idx]), + f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", + f"{subnet_state.stake_weight[idx]:.4f}", + # str(subnet_state.dividends[idx]), + f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", + str(subnet_state.incentives[idx]), + f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", + f"{subnet_state.hotkeys[idx]}", + f"{subnet_state.coldkeys[idx]}", + ) + ) + # Add columns to the table + table.add_column("UID", style="grey89", no_wrap=True, justify="center") + table.add_column( + f"TAO({Balance.get_unit(0)})", + style="medium_purple", + no_wrap=True, + justify="right", + ) + table.add_column( + f"Stake({Balance.get_unit(netuid_)})", + style="green", + no_wrap=True, + justify="right", + ) + table.add_column( + f"Weight({Balance.get_unit(0)}•{Balance.get_unit(netuid_)})", + style="blue", + no_wrap=True, + justify="center", + ) + table.add_column( + "Dividends", style="rgb(181,137,0)", no_wrap=True, justify="center" + ) + table.add_column( + "Incentive", style="rgb(220,50,47)", no_wrap=True, justify="center" + ) + table.add_column( + f"Emission ({Balance.get_unit(netuid_)})", + style="aquamarine3", + no_wrap=True, + justify="center", + ) + table.add_column( + "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + ) + table.add_column( + "Coldkey", style="bold dark_green", no_wrap=True, justify="center" + ) + for row in rows: + table.add_row(*row) + + # Print the table + # bt.__console__.print("\n\n\n") + # bt.__console__.print(subnet_info_table) + console.print("\n\n") + console.print(table) + console.print("\n") + console.print( + f"Subnet: {netuid_}:\n Owner: [light_salmon3]{subnet_info.owner}[/light_salmon3]\n Total Locked: [green]{subnet_info.total_locked}[/green]\n Owner Locked: [green]{subnet_info.owner_locked}[/green]" + ) + console.print( + """ +Description: + The table displays the subnet participants and their metrics. + The columns are as follows: + - UID: The hotkey index in the subnet. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on this subnet. + - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. + - Dividends: Validating dividends earned by the hotkey. + - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) + - Emission: The emission accrued to this hokey on this subnet every block (in staking units). + - Hotkey: The hotkey ss58 address. +""" + ) + + if netuid == 0: + await show_root() + else: + await show_subnet(netuid) + + async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( From 0acb2b4442d8ddde29c861f3302a60c87113a671 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 18:43:09 +0200 Subject: [PATCH 028/332] Updated requirements and use quotes --- bittensor_cli/src/bittensor/extrinsics/registration.py | 2 +- requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index d1a0629c..112ce4fd 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -676,7 +676,7 @@ async def get_neuron_for_pubkey_and_subnet(): async def burned_register_extrinsic( - subtensor: SubtensorInterface, + subtensor: "SubtensorInterface", wallet: Wallet, netuid: int, old_balance: Balance, diff --git a/requirements.txt b/requirements.txt index 2e891bcf..c3a5dfc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ wheel async-property==0.2.2 aiohttp~=3.10.2 -backoff~=2.2.1 GitPython>=3.0.0 fuzzywuzzy~=0.18.0 netaddr~=1.3.0 From 28ca04a6b6e3289a1bacac2563f596b7c2d56f0a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 19:03:59 +0200 Subject: [PATCH 029/332] Scrapped brahmi script. --- bittensor_cli/src/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index acd8cd90..b937220b 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -446,12 +446,6 @@ class WalletValidationTypes(Enum): "\u1684", # ᚄ (Sail, willow, 94) "\u1685", # ᚅ (Nion, ash, 95) "\u169b", # ᚛ (Forfeda, 96) - # Brahmi Script TODO verify these https://discord.com/channels/799672011265015819/1176889593136693339/1288500713625878558 - "\u11000", # 𑀀 (A, 122) - "\u11001", # 𑀁 (Aa, 123) - "\u11002", # 𑀂 (I, 124) - "\u11003", # 𑀃 (Ii, 125) - "\u11005", # 𑀅 (U, 126) # Tifinagh Alphabet "\u2d30", # ⴰ (Ya, 127) "\u2d31", # ⴱ (Yab, 128) From 407e0115279891db5f11afdbfbbf9d39d98de119 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 21:45:24 +0200 Subject: [PATCH 030/332] Add optional netuid grabber. --- bittensor_cli/cli.py | 55 ++++++++++++++++++++--- bittensor_cli/src/commands/stake/stake.py | 5 ++- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index aa798d19..b9304619 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -159,10 +159,22 @@ class Options: "-n", help="Set the netuid(s) to exclude. Separate multiple netuids with a comma, for example: `-n 0,1,2`.", ) - netuid = typer.Option( + netuid = ( + typer.Option( + None, + help="The netuid of the subnet in the root network, (e.g. 1).", + prompt=True, + ), + ) + netuid_not_req = typer.Option( None, help="The netuid of the subnet in the root network, (e.g. 1).", - prompt=True, + prompt=False, + ) + all_netuids = typer.Option( + False, + help="Use all netuids", + prompt=False, ) weights = typer.Option( None, @@ -270,6 +282,25 @@ def verbosity_console_handler(verbosity_level: int = 1) -> None: verbose_console.quiet = False +def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[int]: + """ + Parses options to determine if the user wants to use a specific netuid or all netuids (None) + + Returns: + None if using all netuids, otherwise int for the netuid to use + """ + if netuid is None and all_netuids is True: + return None + elif netuid is None and all_netuids is False: + return typer.prompt( + "Enter the netuid to use. Leave blank for all netuids.", + default=None, + show_default=False, + ) + else: + return netuid + + def get_n_words(n_words: Optional[int]) -> int: """ Prompts the user to select the number of words used in the mnemonic if not supplied or not within the @@ -2384,7 +2415,8 @@ def stake_add( help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying " "hotkeys in `--include-hotkeys`.", ), - netuid: Optional[int] = Options.netuid, + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2405,6 +2437,7 @@ def stake_add( [green]$[/green] btcli stake add --amount 100 --wallet-name --wallet-hotkey """ self.verbosity_handler(quiet, verbose) + netuid = get_optional_netuid(netuid, all_netuids) if stake_all and amount: err_console.print( @@ -2508,6 +2541,8 @@ def stake_remove( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, unstake_all: bool = typer.Option( False, "--unstake-all", @@ -2561,6 +2596,7 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) + netuid = get_optional_netuid(netuid, all_netuids) if all_hotkeys and include_hotkeys: err_console.print( @@ -2627,29 +2663,34 @@ def stake_remove( ) if include_hotkeys: - include_hotkeys = parse_to_list( + included_hotkeys = parse_to_list( include_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + included_hotkeys = [] if exclude_hotkeys: - exclude_hotkeys = parse_to_list( + excluded_hotkeys = parse_to_list( exclude_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--exclude-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + excluded_hotkeys = [] return self._run_command( stake.unstake( wallet, self.initialize_chain(network), hotkey_ss58_address, + netuid, all_hotkeys, - include_hotkeys, - exclude_hotkeys, + included_hotkeys, + excluded_hotkeys, amount, keep_stake, unstake_all, diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 07c9891d..11b91a6a 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1145,6 +1145,7 @@ async def unstake( wallet: Wallet, subtensor: "SubtensorInterface", hotkey_ss58_address: str, + netuid: Optional[int], all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], @@ -1154,7 +1155,9 @@ async def unstake( prompt: bool, ): """Unstake token of amount from hotkey(s).""" - + netuids = ( + [netuid] if netuid is not None else await subtensor.get_all_subnet_netuids() + ) # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] if hotkey_ss58_address: From 7370971fc3e247cb8506802d96a35a905e981741 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 22:14:39 +0200 Subject: [PATCH 031/332] Stake remove --- bittensor_cli/src/commands/stake/stake.py | 274 ++++++++++++++++------ 1 file changed, 206 insertions(+), 68 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 11b91a6a..95b14635 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -8,7 +8,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm, FloatPrompt +from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Table, Column import typer @@ -28,6 +28,7 @@ render_tree, u16_normalized_float, validate_coldkey_presence, + format_error_message, ) if TYPE_CHECKING: @@ -1205,81 +1206,218 @@ async def unstake( final_hotkeys: list[tuple[str, str]] = [] final_amounts: list[Union[float, Balance]] = [] hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - with suppress(ValueError): - with console.status( - f":satellite:Syncing with chain {subtensor}", spinner="earth" - ) as status: - print_verbose("Fetching stake", status) - block_hash = await subtensor.substrate.get_chain_head() - hotkey_stakes = await asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for hotkey in hotkeys_to_unstake_from - ] + + # Get old staking balance. + table = Table( + title=f"[white]Unstake operation to Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", + width=console.width - 5, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + rows = [] + unstake_amount_balance = [] + current_stake_balances = [] + total_received_amount = Balance.from_tao(0) + current_wallet_balance: Balance = ( + await subtensor.get_balance(wallet.coldkeypub.ss58_address) + )[wallet.coldkeypub.ss58_address] + max_float_slippage = 0 + non_zero_netuids = [] + # TODO gather this all + for hotkey in hotkeys_to_unstake_from: + staking_address_name, staking_address_ss58 = hotkey + for netuid in netuids: + # Check that the subnet exists. + dynamic_info = await subtensor.get_subnet_dynamic_info(netuid) + if dynamic_info is None: + console.print(f"[red]Subnet: {netuid} does not exist.[/red]") + return False + + current_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58_address, + netuid=netuid, + ) ) - for hotkey, hotkey_stake in zip(hotkeys_to_unstake_from, hotkey_stakes): - unstake_amount_tao: float = amount - - if unstake_all: - unstake_amount_tao = hotkey_stake.tao - if keep_stake: - # Get the current stake of the hotkey from this coldkey. - unstake_amount_tao = hotkey_stake.tao - keep_stake - amount = unstake_amount_tao - if unstake_amount_tao < 0: - # Skip if max_stake is greater than current stake. - continue - else: - if unstake_amount_tao > hotkey_stake.tao: - # Skip if the specified amount is greater than the current stake. - continue + if current_stake_balance.tao == 0: + continue + non_zero_netuids.append(netuid) + current_stake_balances.append(current_stake_balance) - final_amounts.append(unstake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. + # Determine the amount we are staking. + if amount: + amount_to_unstake_as_balance = Balance.from_tao(amount) + elif unstake_all: + amount_to_unstake_as_balance = current_stake_balance + else: # TODO max_stake + if Confirm.ask( + f"Unstake all: [bold]{current_stake_balance}[/bold]" + f" from [bold]{staking_address_name}[/bold] on netuid: {netuid}?" + ): + amount_to_unstake_as_balance = current_stake_balance + else: + try: + amount = float( + Prompt.ask( + f"Enter amount to unstake in {Balance.get_unit(netuid)} from subnet: {netuid}" + ) + ) + amount_to_unstake_as_balance = Balance.from_tao(amount) + except ValueError: + err_console.print( + ":cross_mark:[red]Invalid amount Please use `--amount` with `--no_prompt`.[/red]" + ) + return False + unstake_amount_balance.append(amount_to_unstake_as_balance) - if len(final_hotkeys) == 0: - # No hotkeys to unstake from. - err_console.print( - "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." + # Check enough to stake. + amount_to_unstake_as_balance.set_unit(netuid) + if amount_to_unstake_as_balance > current_stake_balance: + err_console.print( + f"[red]Not enough stake to remove[/red]:[bold white]\n stake balance:{current_stake_balance}" + f" < unstaking amount: {amount_to_unstake_as_balance}[/bold white]" + ) + return False + + received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( + amount_to_unstake_as_balance ) - return None + total_received_amount += received_amount + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = f"{slippage_pct_float}%" + max_float_slippage = max(max_float_slippage, slippage_pct_float) - # Ask to unstake - if prompt: - if not Confirm.ask( - f"Do you want to unstake from the following keys to {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " - f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] + rows.append( + ( + str(netuid), + # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", + f"{staking_address_ss58}", + str(amount_to_unstake_as_balance), + str(float(dynamic_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", + str(received_amount), + str(slippage_pct), ) - ): - return None - if len(final_hotkeys) == 1: - # do regular unstake - await unstake_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if unstake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=prompt, ) - else: - await unstake_multiple_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if unstake_all else final_amounts, - wait_for_inclusion=True, - prompt=prompt, + + table.add_column("Netuid", justify="center", style="grey89") + table.add_column("Hotkey", justify="center", style="light_salmon3") + table.add_column( + f"Amount ({Balance.get_unit(1)})", justify="center", style="dark_sea_green" + ) + table.add_column( + f"Rate ({Balance.get_unit(0)}/{bt.Balance.get_unit(1)})", + justify="center", + style="light_goldenrod2", + ) + table.add_column( + f"Recieved ({Balance.get_unit(0)})", + justify="center", + style="light_slate_blue", + footer=f"{total_received_amount}", + ) + table.add_column("Slippage", justify="center", style="rgb(220,50,47)") + for row in rows: + table.add_row(*row) + bt.__console__.print(table) + message = "" + if max_float_slippage > 5: + message += f"-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold][yellow]WARNING:[/yellow]\tThe slippage on one of your operations is high: [bold red]{max_float_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" + message += f"-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + if prompt: + if not Confirm.ask("Would you like to continue?"): + return False + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake remove operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are unstaking from. + - [bold white]Amount[/bold white]: The stake amount you are removing from this key. + - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + + # Perform staking operation. + wallet.unlock_coldkey() + with console.status( + f"\n:satellite: Unstaking {amount_to_unstake_as_balance} from {staking_address_name} on netuid: {netuid} ..." + ): + for netuid_i, amount, current in list( + zip(non_zero_netuids, unstake_amount_balance, current_stake_balances) + ): + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58_address, + "netuid": netuid_i, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey ) + response = subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + else: + response.process_events() + if not response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(response.error_message, subtensor.substrate)}" + ) + else: + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58_address, + netuid=netuid_i, + ) + ).set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + console.print( + f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): From 95df386f6064c3b5637abd0d20bc1dbdfcdb8bc2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 22:19:22 +0200 Subject: [PATCH 032/332] Comments. --- bittensor_cli/src/commands/stake/stake.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 95b14635..7a1ba3e6 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1274,6 +1274,7 @@ async def unstake( amount_to_unstake_as_balance = current_stake_balance else: try: + # TODO improve this amount = float( Prompt.ask( f"Enter amount to unstake in {Balance.get_unit(netuid)} from subnet: {netuid}" @@ -1282,7 +1283,7 @@ async def unstake( amount_to_unstake_as_balance = Balance.from_tao(amount) except ValueError: err_console.print( - ":cross_mark:[red]Invalid amount Please use `--amount` with `--no_prompt`.[/red]" + ":cross_mark:[red]Invalid amount Please use `--amount` with `--no-prompt`.[/red]" ) return False unstake_amount_balance.append(amount_to_unstake_as_balance) From 6f25103d761e4c7c433e3cbb565230ad9706808b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 21 Oct 2024 23:17:28 +0200 Subject: [PATCH 033/332] Stake Move --- bittensor_cli/cli.py | 37 +++ .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 228 +++++++++++++++++- 3 files changed, 261 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b9304619..547b8fee 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2698,6 +2698,43 @@ def stake_remove( ) ) + def stake_move( + self, + network=Options.network, + wallet_name=Options.wallet_name, + wallet_path=Options.wallet_path, + wallet_hotkey=Options.wallet_hotkey, + origin_netuid: int = typer.Option(help="Origin netuid", prompt=True), + destination_netuid: int = typer.Option(help="Destination netuid", prompt=True), + destination_hotkey: str = typer.Option( # TODO also accept name + help="Destination hotkey", prompt=True + ), + amount: Optional[float] = typer.Option(help="Amount", prompt=False), + stake_all: bool = typer.Option( + False, "--stake-all", "--all", help="Stake all", prompt=False + ), + prompt: bool = Options.prompt, + ): + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET_AND_HOTKEY, + ) + return self._run_command( + stake.move_stake( + subtensor=self.initialize_chain(network), + wallet=wallet, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + destination_hotkey=destination_hotkey, + amount=amount, + stake_all=stake_all, + prompt=prompt, + ) + ) + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 8e76171b..253f6808 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1122,7 +1122,7 @@ async def get_delegates_by_netuid_light( async def get_subnet_dynamic_info( self, netuid: int, block_hash: Optional[str] = None - ) -> Optional["DynamicInfo"]: + ) -> "DynamicInfo": json = await self.substrate.rpc_request( method="subnetInfo_getDynamicInfo", params=[netuid, block_hash] ) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 7a1ba3e6..541fcded 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1377,7 +1377,7 @@ async def unstake( for netuid_i, amount, current in list( zip(non_zero_netuids, unstake_amount_balance, current_stake_balances) ): - call = subtensor.substrate.compose_call( + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", call_params={ @@ -1386,17 +1386,17 @@ async def unstake( "amount_unstaked": amount.rao, }, ) - extrinsic = subtensor.substrate.create_signed_extrinsic( + extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) - response = subtensor.substrate.submit_extrinsic( + response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") else: - response.process_events() - if not response.is_success: + await response.process_events() + if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " f"{format_error_message(response.error_message, subtensor.substrate)}" @@ -1646,3 +1646,221 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). """ ) + + +async def move_stake( + subtensor: "SubtensorInterface", + wallet: Wallet, + origin_netuid: int, + destination_netuid: int, + destination_hotkey: str, + amount: float, + stake_all: bool, + prompt: bool = True, +): + origin_hotkey_ss58 = wallet.hotkey.ss58_address + # Get the wallet stake balances. + origin_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + ) + ).set_unit(origin_netuid) + + destination_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + ) + ).set_unit(destination_netuid) + + # Determine the amount we are moving. + amount_to_move_as_balance = None + if amount: + amount_to_move_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_move_as_balance = origin_stake_balance + else: # max_stake + # TODO improve this + if Confirm.ask(f"Move all: [bold]{origin_stake_balance}[/bold]?"): + amount_to_move_as_balance = origin_stake_balance + else: + try: + amount = float( + Prompt.ask( + f"Enter amount to move in {Balance.get_unit(origin_netuid)}" + ) + ) + amount_to_move_as_balance = Balance.from_tao(amount) + except ValueError: + err_console.print(f":cross_mark:[red]Invalid amount: {amount}[/red]") + return False + + # Check enough to move. + amount_to_move_as_balance.set_unit(origin_netuid) + if amount_to_move_as_balance > origin_stake_balance: + err_console.print( + f"[red]Not enough stake[/red]:[bold white]\n stake balance:{origin_stake_balance} < moving amount: {amount_to_move_as_balance}[/bold white]" + ) + return False + + # Slippage warning + if prompt: + if origin_netuid == destination_netuid: + received_amount_destination = amount_to_move_as_balance + slippage_pct_float = 0 + slippage_pct = f"{slippage_pct_float}%" + price = Balance.from_tao(1).set_unit(origin_netuid) + price_str = ( + str(float(price.tao)) + + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + else: + dynamic_origin, dynamic_destination = await asyncio.gather( + subtensor.get_subnet_dynamic_info(origin_netuid), + subtensor.get_subnet_dynamic_info(destination_netuid), + ) + price = float(dynamic_origin.price) * 1 / float(dynamic_destination.price) + received_amount_tao, slippage = dynamic_origin.alpha_to_tao_with_slippage( + amount_to_move_as_balance + ) + received_amount_destination, slippage = ( + dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) + ) + received_amount_destination.set_unit(destination_netuid) + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount_destination) + if slippage + received_amount_destination != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + price_str = ( + str(float(price)) + + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + + table = Table( + title="[white]Move Stake", + width=console.width - 5, + safe_box=True, + padding=(0, 1), + collapse_padding=False, + pad_edge=True, + expand=True, + show_header=True, + show_footer=True, + show_edge=False, + show_lines=False, + leading=0, + style="none", + row_styles=None, + header_style="bold", + footer_style="bold", + border_style="rgb(7,54,66)", + title_style="bold magenta", + title_justify="center", + highlight=False, + ) + table.add_column("origin netuid", justify="center", style="rgb(133,153,0)") + table.add_column("origin hotkey", justify="center", style="rgb(38,139,210)") + table.add_column("dest netuid", justify="center", style="rgb(133,153,0)") + table.add_column("dest hotkey", justify="center", style="rgb(38,139,210)") + table.add_column( + f"amount ({Balance.get_unit(origin_netuid)})", + justify="center", + style="rgb(38,139,210)", + ) + table.add_column( + f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", + justify="center", + style="rgb(42,161,152)", + ) + table.add_column( + f"received ({Balance.get_unit(destination_netuid)})", + justify="center", + style="rgb(220,50,47)", + ) + table.add_column("slippage", justify="center", style="rgb(181,137,0)") + + table.add_row( + f"{Balance.get_unit(origin_netuid)}({origin_netuid})", + f"{origin_hotkey_ss58[:3]}...{origin_hotkey_ss58[-3:]}", + # TODO f-strings + Balance.get_unit(destination_netuid) + "(" + str(destination_netuid) + ")", + f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}", + str(amount_to_move_as_balance), + price_str, + str(received_amount_destination.set_unit(destination_netuid)), + str(slippage_pct), + ) + + console.print(table) + message = "" + if slippage_pct_float > 5: + message += "\t-------------------------------------------------------------------------------------------------------------------\n" + message += f"\t[bold][yellow]WARNING:[/yellow]\tSlippage is high: [bold red]{slippage_pct}[/bold red], this may result in a loss of funds.[/bold] \n" + message += "\t-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + if not Confirm.ask("Would you like to continue?"): + return True + + # Perform staking operation. + wallet.unlock_coldkey() + with console.status( + f"\n:satellite: Moving {amount_to_move_as_balance} from {origin_hotkey_ss58} on netuid: {origin_netuid} to " + f"{destination_hotkey} on netuid: {destination_netuid} ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey, + "destination_netuid": destination_netuid, + "amount_moved": amount_to_move_as_balance.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + else: + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error:" + f" {format_error_message(response.error_message, subtensor.substrate)}" + ) + return + else: + new_origin_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + ) + ).set_unit(origin_netuid) + new_destination_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + ) + ).set_unit(destination_netuid) + console.print( + f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " + f"[green]{new_origin_stake_balance}[/green]" + ) + console.print( + f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " + f"[green]{new_destination_stake_balance}[/green]" + ) + return From 0560a58d2af763a76b32cfc2023210bff6944cb9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 21 Oct 2024 14:24:49 -0700 Subject: [PATCH 034/332] wallet balance --- .../src/bittensor/subtensor_interface.py | 51 +++++++++++++------ bittensor_cli/src/commands/wallets.py | 1 + 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 253f6808..182cfc1d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -381,21 +381,42 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - calls = [ - ( - await self.substrate.create_storage_key( - "SubtensorModule", - "TotalColdkeyStake", - [address], - block_hash=block_hash, - ) - ) - for address in ss58_addresses - ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + sub_stakes = await self.get_stake_info_for_coldkeys( + ss58_addresses, block_hash=block_hash + ) + + # Token pricing info + dynamic_info = await self.get_all_subnet_dynamic_info() + results = {} - for item in batch_call: - results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) + + for ss58, stake_info_list in sub_stakes.items(): + all_staked_tao = 0 + + for sub_stake in stake_info_list: + if sub_stake.stake.rao == 0: + continue + netuid = sub_stake.netuid + pool = dynamic_info[netuid] + + alpha_value = Balance.from_rao(int(sub_stake.stake.rao)).set_unit( + netuid + ) + + tao_locked = pool.tao_in + + issuance = pool.alpha_out if pool.is_dynamic else tao_locked + tao_ownership = 0 + + if alpha_value.tao > 0.00009 and issuance.tao != 0: + tao_ownership = Balance.from_tao( + (alpha_value.tao / issuance.tao) * tao_locked.tao + ) + + all_staked_tao += tao_ownership.rao + + results[ss58] = Balance.from_rao(all_staked_tao) + return results async def get_total_stake_for_hotkey( @@ -1230,7 +1251,7 @@ async def get_stake_info_for_coldkeys( hex_bytes_result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkeys", - params=encoded_coldkeys, + params=[encoded_coldkeys], block_hash=block_hash, ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3faefd23..954bdb6c 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -253,6 +253,7 @@ async def wallet_balance( subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), ) + total_free_balance = sum(free_balances.values()) total_staked_balance = sum(staked_balances.values()) From 83c044cd577cf1bb5b7abb4f5dfe20746f96060e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 21 Oct 2024 15:49:12 -0700 Subject: [PATCH 035/332] btcli sudo senate --- bittensor_cli/cli.py | 20 ++++++++ bittensor_cli/src/__init__.py | 1 + bittensor_cli/src/commands/sudo.py | 75 +++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 547b8fee..29ebc4dd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -670,6 +670,9 @@ def __init__(self): self.sudo_app.command("get", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_get ) + self.sudo_app.command( + "senate", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_senate) # subnets commands self.subnets_app.command( @@ -3109,6 +3112,23 @@ def sudo_get( sudo.get_hyperparameters(self.initialize_chain(network), netuid) ) + def sudo_senate( + self, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Shows the Senate members of the Bittensor's governance protocol. + + This command lists the delegates involved in the decision-making process of the Bittensor network, showing their names and wallet addresses. This information is crucial for understanding who holds governance roles within the network. + + EXAMPLE + [green]$[/green] btcli root senate + """ + self.verbosity_handler(quiet, verbose) + return self._run_command(sudo.get_senate(self.initialize_chain(network))) + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index b937220b..1caee07f 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -513,6 +513,7 @@ class WalletValidationTypes(Enum): }, "SUDO": { "CONFIG": "Subnet Configuration", + "GOVERNANCE": "Governance" }, "SUBNETS": { "INFO": "Subnet Information", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6ebbb0ec..0bf3199b 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,12 +1,12 @@ import asyncio -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Union, Optional from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich import box from rich.table import Column, Table -from bittensor_cli.src import HYPERPARAMS +from bittensor_cli.src import HYPERPARAMS, DelegatesDetails from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -234,3 +234,74 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): console.print(table) return True + + +async def get_senate(subtensor: "SubtensorInterface"): + """View Bittensor's senate memebers""" + with console.status( + f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", + spinner="aesthetic", + ) as status: + print_verbose("Fetching senate members", status) + senate_members = await _get_senate_members(subtensor) + + print_verbose("Fetching member details from Github and on-chain identities") + delegate_info: dict[ + str, DelegatesDetails + ] = await subtensor.get_delegate_identities() + + table = Table( + Column( + "[bold white]NAME", + style="bright_cyan", + no_wrap=True, + ), + Column( + "[bold white]ADDRESS", + style="bright_magenta", + no_wrap=True, + ), + title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", + show_footer=True, + show_edge=False, + expand=False, + border_style="bright_black", + leading=True, + ) + + for ss58_address in senate_members: + table.add_row( + ( + delegate_info[ss58_address].display + if ss58_address in delegate_info + else "~" + ), + ss58_address, + ) + + return console.print(table) + + +async def _get_senate_members( + subtensor: "SubtensorInterface", block_hash: Optional[str] = None +) -> list[str]: + """ + Gets all members of the senate on the given subtensor's network + + :param subtensor: SubtensorInterface object to use for the query + + :return: list of the senate members' ss58 addresses + """ + senate_members = await subtensor.substrate.query( + module="SenateMembers", + storage_function="Members", + params=None, + block_hash=block_hash, + ) + try: + return [ + decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) + ] + except (IndexError, TypeError): + err_console.print("Unable to retrieve senate members.") + return [] From 6ab88518a86e326c49f328f8de263688ffcc4b06 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 21 Oct 2024 16:30:01 -0700 Subject: [PATCH 036/332] btcli root proposals --- bittensor_cli/cli.py | 22 +++- bittensor_cli/src/commands/sudo.py | 178 +++++++++++++++++++++++++---- 2 files changed, 178 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 29ebc4dd..460c1bb8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -673,6 +673,9 @@ def __init__(self): self.sudo_app.command( "senate", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] )(self.sudo_senate) + self.sudo_app.command( + "proposals", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_proposals) # subnets commands self.subnets_app.command( @@ -3124,11 +3127,28 @@ def sudo_senate( This command lists the delegates involved in the decision-making process of the Bittensor network, showing their names and wallet addresses. This information is crucial for understanding who holds governance roles within the network. EXAMPLE - [green]$[/green] btcli root senate + [green]$[/green] btcli sudo senate """ self.verbosity_handler(quiet, verbose) return self._run_command(sudo.get_senate(self.initialize_chain(network))) + def sudo_proposals( + self, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + View active proposals for the senate in the Bittensor's governance protocol. + + This command displays the details of ongoing proposals, including proposal hashes, votes, thresholds, and proposal data. + + EXAMPLE + [green]$[/green] btcli sudo proposals + """ + self.verbosity_handler(quiet, verbose) + return self._run_command(sudo.proposals(self.initialize_chain(network))) + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 0bf3199b..1de277da 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -5,6 +5,7 @@ from bittensor_wallet.errors import KeyFileError from rich import box from rich.table import Column, Table +from scalecodec import GenericCall from bittensor_cli.src import HYPERPARAMS, DelegatesDetails from bittensor_cli.src.bittensor.chain_data import decode_account_id @@ -17,7 +18,10 @@ ) if TYPE_CHECKING: - from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + ProposalVoteData, + ) # helpers and extrinsics @@ -167,6 +171,110 @@ async def set_hyperparameter_extrinsic( return True +async def _get_senate_members( + subtensor: "SubtensorInterface", block_hash: Optional[str] = None +) -> list[str]: + """ + Gets all members of the senate on the given subtensor's network + + :param subtensor: SubtensorInterface object to use for the query + + :return: list of the senate members' ss58 addresses + """ + senate_members = await subtensor.substrate.query( + module="SenateMembers", + storage_function="Members", + params=None, + block_hash=block_hash, + ) + try: + return [ + decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) + ] + except (IndexError, TypeError): + err_console.print("Unable to retrieve senate members.") + return [] + + +async def _get_proposals( + subtensor: "SubtensorInterface", block_hash: str +) -> dict[str, tuple[dict, "ProposalVoteData"]]: + async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: + proposal_data = await subtensor.substrate.query( + module="Triumvirate", + storage_function="ProposalOf", + block_hash=block_hash, + params=[p_hash], + ) + return proposal_data + + ph = await subtensor.substrate.query( + module="Triumvirate", + storage_function="Proposals", + params=None, + block_hash=block_hash, + ) + + try: + proposal_hashes: list[str] = [ + f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0])) + ] + except (IndexError, TypeError): + err_console.print("Unable to retrieve proposal vote data") + return {} + + call_data_, vote_data_ = await asyncio.gather( + asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]), + asyncio.gather(*[subtensor.get_vote_data(h) for h in proposal_hashes]), + ) + return { + proposal_hash: (cd, vd) + for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes) + } + + +def display_votes( + vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] +) -> str: + vote_list = list() + + for address in vote_data.ayes: + vote_list.append( + "{}: {}".format( + delegate_info[address].display if address in delegate_info else address, + "[bold green]Aye[/bold green]", + ) + ) + + for address in vote_data.nays: + vote_list.append( + "{}: {}".format( + delegate_info[address].display if address in delegate_info else address, + "[bold red]Nay[/bold red]", + ) + ) + + return "\n".join(vote_list) + + +def format_call_data(call_data: dict) -> str: + # Extract the module and call details + module, call_details = next(iter(call_data.items())) + + # Extract the call function name and arguments + call_info = call_details[0] + call_function, call_args = next(iter(call_info.items())) + + # Extract the argument, handling tuple values + formatted_args = ", ".join( + str(arg[0]) if isinstance(arg, tuple) else str(arg) + for arg in call_args.values() + ) + + # Format the final output string + return f"{call_function}({formatted_args})" + + # commands @@ -282,26 +390,54 @@ async def get_senate(subtensor: "SubtensorInterface"): return console.print(table) -async def _get_senate_members( - subtensor: "SubtensorInterface", block_hash: Optional[str] = None -) -> list[str]: - """ - Gets all members of the senate on the given subtensor's network +async def proposals(subtensor: "SubtensorInterface"): + console.print( + ":satellite: Syncing with chain: [white]{}[/white] ...".format( + subtensor.network + ) + ) + print_verbose("Fetching senate members & proposals") + block_hash = await subtensor.substrate.get_chain_head() + senate_members, all_proposals = await asyncio.gather( + _get_senate_members(subtensor, block_hash), + _get_proposals(subtensor, block_hash), + ) - :param subtensor: SubtensorInterface object to use for the query + print_verbose("Fetching member information from Chain") + registered_delegate_info: dict[ + str, DelegatesDetails + ] = await subtensor.get_delegate_identities() - :return: list of the senate members' ss58 addresses - """ - senate_members = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - block_hash=block_hash, + table = Table( + Column( + "[white]HASH", + style="light_goldenrod2", + no_wrap=True, + ), + Column("[white]THRESHOLD", style="rgb(42,161,152)"), + Column("[white]AYES", style="green"), + Column("[white]NAYS", style="red"), + Column( + "[white]VOTES", + style="rgb(50,163,219)", + ), + Column("[white]END", style="bright_cyan"), + Column("[white]CALLDATA", style="dark_sea_green"), + title=f"\n[dark_orange]Proposals\t\t\nActive Proposals: {len(all_proposals)}\t\tSenate Size: {len(senate_members)}\nNetwork: {subtensor.network}", + show_footer=True, + box=box.SIMPLE_HEAVY, + pad_edge=False, + width=None, + border_style="bright_black", ) - try: - return [ - decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) - ] - except (IndexError, TypeError): - err_console.print("Unable to retrieve senate members.") - return [] + for hash_, (call_data, vote_data) in all_proposals.items(): + table.add_row( + hash_, + str(vote_data.threshold), + str(len(vote_data.ayes)), + str(len(vote_data.nays)), + display_votes(vote_data, registered_delegate_info), + str(vote_data.end), + format_call_data(call_data), + ) + return console.print(table) From 4197bf9a6889b6200225da24b2f12feab882da22 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 21 Oct 2024 16:41:15 -0700 Subject: [PATCH 037/332] updated subnet creation --- bittensor_cli/src/commands/subnets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 1e7cda68..7e539a90 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -113,7 +113,10 @@ async def _find_event_attributes_in_extrinsic_receipt( call = await substrate.compose_call( call_module="SubtensorModule", call_function="register_network", - call_params={"immunity_period": 0, "reg_allowed": True}, + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + }, ) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey From 9e6f235467ebabb90f4e0b27caa82a20b503a58d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 21 Oct 2024 17:08:57 -0700 Subject: [PATCH 038/332] btcli sudo senate-vote --- bittensor_cli/cli.py | 54 ++++++++++ bittensor_cli/src/commands/sudo.py | 154 +++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 460c1bb8..c999dc96 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -676,6 +676,9 @@ def __init__(self): self.sudo_app.command( "proposals", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] )(self.sudo_proposals) + self.sudo_app.command( + "senate-vote", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_senate_vote) # subnets commands self.subnets_app.command( @@ -747,6 +750,9 @@ def __init__(self): self.subnets_app.command("lock_cost", hidden=True)(self.subnets_lock_cost) self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) + # Sudo + self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote) + def initialize_chain( self, network: Optional[list[str]] = None, @@ -3149,6 +3155,54 @@ def sudo_proposals( self.verbosity_handler(quiet, verbose) return self._run_command(sudo.proposals(self.initialize_chain(network))) + def sudo_senate_vote( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + proposal: str = typer.Option( + None, + "--proposal", + "--proposal-hash", + prompt="Enter the proposal hash", + help="The hash of the proposal to vote on.", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + vote: bool = typer.Option( + None, + "--vote-aye/--vote-nay", + prompt="Enter y to vote Aye, or enter n to vote Nay", + help="The vote casted on the proposal", + ), + ): + """ + Cast a vote on an active proposal in Bittensor's governance protocol. + + This command is used by Senate members to vote on various proposals that shape the network's future. Use `btcli sudo proposals` to see the active proposals and their hashes. + + USAGE + The user must specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast a 'Yes' or 'No' vote, contributing to the decision-making process on the proposal. This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. + + EXAMPLE + [green]$[/green] btcli sudo senate_vote --proposal + """ + self.verbosity_handler(quiet, verbose) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + return self._run_command( + sudo.senate_vote( + wallet, self.initialize_chain(network), proposal, vote, prompt + ) + ) + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 1de277da..7f7ef5cb 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -5,6 +5,7 @@ from bittensor_wallet.errors import KeyFileError from rich import box from rich.table import Column, Table +from rich.prompt import Confirm from scalecodec import GenericCall from bittensor_cli.src import HYPERPARAMS, DelegatesDetails @@ -275,6 +276,105 @@ def format_call_data(call_data: dict) -> str: return f"{call_function}({formatted_args})" +def _validate_proposal_hash(proposal_hash: str) -> bool: + if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66: + return False + else: + return True + + +async def _is_senate_member(subtensor: "SubtensorInterface", hotkey_ss58: str) -> bool: + """ + Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. + The senate is a key governance body within the Bittensor network, responsible for overseeing and + approving various network operations and proposals. + + :param subtensor: SubtensorInterface object to use for the query + :param hotkey_ss58: The `SS58` address of the neuron's hotkey. + + :return: `True` if the neuron is a senate member at the given block, `False` otherwise. + + This function is crucial for understanding the governance dynamics of the Bittensor network and for + identifying the neurons that hold decision-making power within the network. + """ + + senate_members = await _get_senate_members(subtensor) + + if not hasattr(senate_members, "count"): + return False + + return senate_members.count(hotkey_ss58) > 0 + + +async def vote_senate_extrinsic( + subtensor: "SubtensorInterface", + wallet: Wallet, + proposal_hash: str, + proposal_idx: int, + vote: bool, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False, +) -> bool: + """Votes ayes or nays on proposals. + + :param subtensor: The SubtensorInterface object to use for the query + :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. + :param proposal_hash: The hash of the proposal for which voting data is requested. + :param proposal_idx: The index of the proposal to vote. + :param vote: Whether to vote aye or nay. + :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, + or returns `False` if the extrinsic fails to be finalized within the timeout. + :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + + :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask(f"Cast a vote of {vote}?"): + return False + + with console.status(":satellite: Casting vote..", spinner="aesthetic"): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "proposal": proposal_hash, + "index": proposal_idx, + "approve": vote, + }, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + await asyncio.sleep(0.5) + return False + # Successful vote, final check for data + else: + if vote_data := await subtensor.get_vote_data(proposal_hash): + if ( + vote_data.ayes.count(wallet.hotkey.ss58_address) > 0 + or vote_data.nays.count(wallet.hotkey.ss58_address) > 0 + ): + console.print(":white_heavy_check_mark: [green]Vote cast.[/green]") + return True + else: + # hotkey not found in ayes/nays + err_console.print( + ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" + ) + return False + else: + return False + + # commands @@ -441,3 +541,57 @@ async def proposals(subtensor: "SubtensorInterface"): format_call_data(call_data), ) return console.print(table) + + +async def senate_vote( + wallet: Wallet, + subtensor: "SubtensorInterface", + proposal_hash: str, + vote: bool, + prompt: bool, +) -> bool: + """Vote in Bittensor's governance protocol proposals""" + + if not proposal_hash: + err_console.print( + "Aborting: Proposal hash not specified. View all proposals with the `proposals` command." + ) + return False + elif not _validate_proposal_hash(proposal_hash): + err_console.print( + "Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long" + ) + return False + + print_verbose(f"Fetching senate status of {wallet.hotkey_str}") + if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): + err_console.print( + f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." + ) + return False + + # Unlock the wallet. + try: + wallet.unlock_hotkey() + wallet.unlock_coldkey() + except KeyFileError: + return False + + console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") + vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True) + if not vote_data: + err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return False + + success = await vote_senate_extrinsic( + subtensor=subtensor, + wallet=wallet, + proposal_hash=proposal_hash, + proposal_idx=vote_data.index, + vote=vote, + wait_for_inclusion=True, + wait_for_finalization=False, + prompt=prompt, + ) + + return success From c86d756b3c55496e286ff0c71dd5248b3209fced Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 22 Oct 2024 16:23:23 +0200 Subject: [PATCH 039/332] Get Child hotkeys --- .../src/commands/stake/children_hotkeys.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index c472bcb1..2d6e92fb 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -340,7 +340,7 @@ async def get_take(child: tuple) -> float: async def _render_table( parent_hotkey: str, - netuid_children_tuples: list[tuple[int, list[tuple[int, str]]]], + netuid_children_: list[tuple[int, list[tuple[int, str]]]], ): """ Retrieves and renders children hotkeys and their details for a given parent hotkey. @@ -363,10 +363,11 @@ async def _render_table( "Current Stake Weight", style="bold red", no_wrap=True, justify="right" ) - if not netuid_children_tuples: + if not netuid_children_: console.print(table) console.print( - f"[bold red]There are currently no child hotkeys with parent hotkey: {wallet.name} ({parent_hotkey}).[/bold red]" + f"[bold red]There are currently no child hotkeys with parent hotkey: " + f"{wallet.name} ({parent_hotkey}).[/bold red]" ) return @@ -374,15 +375,15 @@ async def _render_table( total_proportion = 0 total_stake_weight = 0 - netuid_children_tuples.sort( + netuid_children_.sort( key=lambda x: x[0] ) # Sort by netuid in ascending order - for index, (netuid, children_) in enumerate(netuid_children_tuples): + for index, (netuid_, children_) in enumerate(netuid_children_): # calculate totals total_proportion_per_netuid = 0 total_stake_weight_per_netuid = 0 - avg_take_per_netuid = 0 + avg_take_per_netuid = 0.0 hotkey_stake_dict = await subtensor.get_total_stake_for_hotkey( parent_hotkey @@ -427,7 +428,7 @@ async def _render_table( hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") table.add_row( - str(netuid), + str(netuid_), hotkey, proportion_str, take_str, @@ -451,7 +452,7 @@ async def _render_table( total_stake_weight += total_stake_weight_per_netuid # Add a dividing line if there are more than one netuid - if len(netuid_children_tuples) > 1: + if len(netuid_children_) > 1: table.add_section() console.print(table) From 7575ac92e9993a27f03da03637bc244e92732c93 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 22 Oct 2024 16:29:34 +0200 Subject: [PATCH 040/332] Set children --- bittensor_cli/cli.py | 25 +----- bittensor_cli/src/__init__.py | 5 +- .../src/commands/stake/children_hotkeys.py | 85 ++----------------- bittensor_cli/src/commands/wallets.py | 1 - 4 files changed, 11 insertions(+), 105 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c999dc96..18683551 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2814,18 +2814,8 @@ def stake_set_children( wallet_hotkey: str = Options.wallet_hotkey, wallet_path: str = Options.wallet_path, network: Optional[list[str]] = Options.network, - netuid: Optional[int] = typer.Option( - None, - help="The netuid of the subnet, (e.g. 4)", - prompt=False, - ), - all_netuids: bool = typer.Option( - False, - "--all-netuids", - "--all", - "--allnetuids", - help="When this flag is used it sets child hotkeys on all subnets.", - ), + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, proportions: list[float] = typer.Option( [], "--proportions", @@ -2851,15 +2841,8 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 """ self.verbosity_handler(quiet, verbose) - if all_netuids and netuid: - err_console.print("Specify either a netuid or `--all`, not both.") - raise typer.Exit() - if all_netuids: - netuid = None - elif not netuid: - netuid = IntPrompt.ask( - "Enter a netuid (leave blank for all)", default=None, show_default=True - ) + netuid = get_optional_netuid(netuid, all_netuids) + children = list_prompt( children, str, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1caee07f..a2234e7c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -511,10 +511,7 @@ class WalletValidationTypes(Enum): "STAKE_MGMT": "Stake Management", "CHILD": "Child Hotkeys", }, - "SUDO": { - "CONFIG": "Subnet Configuration", - "GOVERNANCE": "Governance" - }, + "SUDO": {"CONFIG": "Subnet Configuration", "GOVERNANCE": "Governance"}, "SUBNETS": { "INFO": "Subnet Information", "CREATION": "Subnet Creation & Management", diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 2d6e92fb..8d260e0a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -375,9 +375,7 @@ async def _render_table( total_proportion = 0 total_stake_weight = 0 - netuid_children_.sort( - key=lambda x: x[0] - ) # Sort by netuid in ascending order + netuid_children_.sort(key=lambda x: x[0]) # Sort by netuid in ascending order for index, (netuid_, children_) in enumerate(netuid_children_): # calculate totals @@ -488,19 +486,20 @@ async def _render_table( return children -async def set_children_new( +async def set_children( wallet: Wallet, subtensor: "SubtensorInterface", children: list[str], proportions: list[float], - hotkey: str, - netuid: int, + netuid: Optional[int], wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, ): """Set children hotkeys.""" # Validate children SS58 addresses + # TODO check to see if this should be allowed to be specified by user instead of pulling from wallet + hotkey = wallet.hotkey.ss58_address for child in children: if not is_valid_ss58_address(child): err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") @@ -516,7 +515,7 @@ async def set_children_new( f"Proposed sum of proportions is {total_proposed}." ) children_with_proportions = list(zip(proportions, children)) - if netuid: + if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, @@ -561,78 +560,6 @@ async def set_children_new( ) -async def set_children( - wallet: Wallet, - subtensor: "SubtensorInterface", - children: list[str], - proportions: list[float], - netuid: Optional[int] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, -): - """Set children hotkeys.""" - # Validate children SS58 addresses - for child in children: - if not is_valid_ss58_address(child): - err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") - return - if child == wallet.hotkey.ss58_address: - err_console.print(":cross_mark:[red] Cannot set yourself as a child.[/red]") - return - - total_proposed = sum(proportions) - if total_proposed > 1: - raise ValueError( - f"Invalid proportion: The sum of all proportions cannot be greater than 1. " - f"Proposed sum of proportions is {total_proposed}." - ) - - children_with_proportions = list(zip(proportions, children)) - if netuid: - success, message = await set_children_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - children_with_proportions=children_with_proportions, - prompt=True, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # Result - if success: - if wait_for_inclusion and wait_for_finalization: - console.print("New Status:") - await get_children(wallet, subtensor, netuid) - console.print( - ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" - ) - else: - console.print( - f":cross_mark:[red] Unable to set children hotkeys.[/red] {message}" - ) - else: - # set children on all subnets that parent is registered on - netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: # dont include root network - continue - console.print(f"Setting children on netuid {netuid}.") - await set_children_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - children_with_proportions=children_with_proportions, - prompt=False, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - console.print( - ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" - ) - - async def revoke_children( wallet: Wallet, subtensor: "SubtensorInterface", diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 954bdb6c..3faefd23 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -253,7 +253,6 @@ async def wallet_balance( subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), ) - total_free_balance = sum(free_balances.values()) total_staked_balance = sum(staked_balances.values()) From ff61a9683aea83b3119d35967a1d4d4eadaaaf69 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 22 Oct 2024 15:46:46 -0700 Subject: [PATCH 041/332] btcli sudo set-take --- bittensor_cli/cli.py | 57 ++++++++ bittensor_cli/src/__init__.py | 6 +- .../src/bittensor/subtensor_interface.py | 30 +++++ bittensor_cli/src/commands/sudo.py | 127 ++++++++++++++++++ 4 files changed, 219 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 18683551..2e642cce 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -679,6 +679,9 @@ def __init__(self): self.sudo_app.command( "senate-vote", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] )(self.sudo_senate_vote) + self.sudo_app.command("set-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( + self.sudo_set_take + ) # subnets commands self.subnets_app.command( @@ -3186,6 +3189,60 @@ def sudo_senate_vote( ) ) + def sudo_set_take( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + take: float = typer.Option(None, help="The new take value."), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Allows users to change their delegate take percentage. + + This command can be used to update the delegate takes individually for every subnet. To run the command, the user must have a configured wallet with both hotkey and coldkey. + The command makes sure the new take value is within 0-18% range. + + EXAMPLE + [green]$[/green] btcli sudo set_take --wallet-name my_wallet --wallet-hotkey my_hotkey + """ + max_value = 0.18 + min_value = 0.00 + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + current_take = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + console.print(f"Current take is [dark_orange]{current_take * 100.:.2f}%") + + if not take: + max_value_style = typer.style(f"Max: {max_value}", fg="magenta") + min_value_style = typer.style(f"Min: {min_value}", fg="magenta") + prompt_text = typer.style( + "Enter take value (0.18 for 18%)", fg="bright_cyan", bold=True + ) + take = FloatPrompt.ask(f"{prompt_text} {min_value_style} {max_value_style}") + + if not (min_value <= take <= max_value): + print_error( + f"Take value must be between {min_value} and {max_value}. Provided value: {take}" + ) + raise typer.Exit() + + return self._run_command( + sudo.set_take(wallet, self.initialize_chain(network), take) + ) + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a2234e7c..3bf9a3c3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -511,7 +511,11 @@ class WalletValidationTypes(Enum): "STAKE_MGMT": "Stake Management", "CHILD": "Child Hotkeys", }, - "SUDO": {"CONFIG": "Subnet Configuration", "GOVERNANCE": "Governance"}, + "SUDO": { + "CONFIG": "Subnet Configuration", + "GOVERNANCE": "Governance", + "TAKE": "Delegate take configuration" + }, "SUBNETS": { "INFO": "Subnet Information", "CREATION": "Subnet Creation & Management", diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 182cfc1d..3e195b65 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -10,6 +10,7 @@ from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset from substrateinterface.exceptions import SubstrateRequestException +from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float import typer from bittensor_cli.src.bittensor.async_substrate_interface import ( @@ -443,6 +444,35 @@ async def get_total_stake_for_hotkey( ) return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} + async def current_take( + self, + hotkey_ss58: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + 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. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int], optional): The blockchain block number for the query. + + Returns: + Optional[float]: The delegate take percentage, None if not available. + + The delegate take is a critical parameter in the network's incentive structure, influencing + the distribution of rewards among neurons and their nominators. + """ + result = await self.substrate.query( + module="SubtensorModule", + storage_function="Delegates", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return u16_normalized_float(result) + async def get_netuids_for_hotkey( self, hotkey_ss58: str, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 7f7ef5cb..c8daf574 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -16,6 +16,7 @@ print_error, print_verbose, normalize_hyperparameters, + u16_normalized_float, ) if TYPE_CHECKING: @@ -375,6 +376,79 @@ async def vote_senate_extrinsic( return False +async def set_take_extrinsic( + subtensor: "SubtensorInterface", + wallet: Wallet, + delegate_ss58: str, + take: float = 0.0, +) -> bool: + """ + Set delegate hotkey take + + :param subtensor: SubtensorInterface (initialized) + :param wallet: The wallet containing the hotkey to be nominated. + :param delegate_ss58: Hotkey + :param take: Delegate take on subnet ID + + :return: `True` if the process is successful, `False` otherwise. + + This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the + dynamic selection and participation of validators in the network's consensus process. + """ + + # Calculate u16 representation of the take + take_u16 = int(take * 0xFFFF) + + print_verbose("Checking current take") + # Check if the new take is greater or lower than existing take or if existing is set + current_take = await get_current_take(subtensor, wallet) + current_take_u16 = int(float(current_take) * 0xFFFF) + + if take_u16 == current_take_u16: + console.print("Nothing to do, take hasn't changed") + return True + + if current_take_u16 < take_u16: + console.print( + f"Current take is [dark_orange]{current_take * 100.:.2f}%[/dark_orange]. Increasing to [dark_orange]{take * 100:.2f}%." + ) + with console.status( + f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": delegate_ss58, + "take": take_u16, + }, + ) + success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + + else: + console.print( + f"Current take is [dark_orange]{current_take * 100.:.2f}%[/dark_orange]. Decreasing to [dark_orange]{take * 100:.2f}%." + ) + with console.status( + f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": delegate_ss58, + "take": take_u16, + }, + ) + success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + + if not success: + err_console.print(err) + else: + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + return success + + # commands @@ -595,3 +669,56 @@ async def senate_vote( ) return success + + +async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet): + current_take = await subtensor.current_take(wallet.hotkey.ss58_address) + return current_take + + +async def set_take( + wallet: Wallet, subtensor: "SubtensorInterface", take: float +) -> bool: + """Set delegate take.""" + + async def _do_set_take() -> bool: + if take > 0.18 or take < 0: + err_console.print("ERROR: Take value should not exceed 18% or be below 0%") + return False + + block_hash = await subtensor.substrate.get_chain_head() + netuids_registered = await subtensor.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, block_hash=block_hash + ) + if not len(netuids_registered) > 0: + err_console.print( + f"Hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered to any subnet. Please register using [dark_orange]`btcli subnets register`[/dark_orange] and try again." + ) + return False + + result: bool = await set_take_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=wallet.hotkey.ss58_address, + take=take, + ) + + if not result: + err_console.print("Could not set the take") + return False + else: + new_take = await get_current_take(subtensor, wallet) + console.print(f"New take is [dark_orange]{new_take * 100.:.2f}%") + return True + + console.print(f"Setting take on [dark_orange]network: {subtensor.network}") + + try: + wallet.unlock_hotkey() + wallet.unlock_coldkey() + except KeyFileError: + return False + + result_ = await _do_set_take() + + return result_ From 588cadd7c43514a26dae0207ce5a0258428ba01b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 22 Oct 2024 16:17:08 -0700 Subject: [PATCH 042/332] btcli sudo get-take --- bittensor_cli/cli.py | 41 ++++++++++++++++++- .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/commands/sudo.py | 1 - 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2e642cce..d8065829 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -682,6 +682,9 @@ def __init__(self): self.sudo_app.command("set-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( self.sudo_set_take ) + self.sudo_app.command("get-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( + self.sudo_get_take + ) # subnets commands self.subnets_app.command( @@ -755,6 +758,8 @@ def __init__(self): # Sudo self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote) + self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) + self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) def initialize_chain( self, @@ -3202,11 +3207,11 @@ def sudo_set_take( """ Allows users to change their delegate take percentage. - This command can be used to update the delegate takes individually for every subnet. To run the command, the user must have a configured wallet with both hotkey and coldkey. + This command can be used to update the delegate takes. To run the command, the user must have a configured wallet with both hotkey and coldkey. The command makes sure the new take value is within 0-18% range. EXAMPLE - [green]$[/green] btcli sudo set_take --wallet-name my_wallet --wallet-hotkey my_hotkey + [green]$[/green] btcli sudo set-take --wallet-name my_wallet --wallet-hotkey my_hotkey """ max_value = 0.18 min_value = 0.00 @@ -3243,6 +3248,38 @@ def sudo_set_take( sudo.set_take(wallet, self.initialize_chain(network), take) ) + def sudo_get_take( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Allows users to check their delegate take percentage. + + This command can be used to fetch the delegate take of your hotkey. + + EXAMPLE + [green]$[/green] btcli sudo get-take --wallet-name my_wallet --wallet-hotkey my_hotkey + """ + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + current_take = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + console.print(f"Current take is [dark_orange]{current_take * 100.:.2f}%") + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 3e195b65..74fd7a78 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -10,7 +10,6 @@ from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset from substrateinterface.exceptions import SubstrateRequestException -from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float import typer from bittensor_cli.src.bittensor.async_substrate_interface import ( @@ -38,6 +37,7 @@ err_console, decode_hex_identity_dict, validate_chain_endpoint, + u16_normalized_float, ) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index c8daf574..641fd7ba 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -16,7 +16,6 @@ print_error, print_verbose, normalize_hyperparameters, - u16_normalized_float, ) if TYPE_CHECKING: From c2e4b6d49e2a46fc5092559494612bd47742e3d4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 23 Oct 2024 20:25:32 +0200 Subject: [PATCH 043/332] Scale decoding. --- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 95 +++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 3bf9a3c3..20cb5652 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -514,7 +514,7 @@ class WalletValidationTypes(Enum): "SUDO": { "CONFIG": "Subnet Configuration", "GOVERNANCE": "Governance", - "TAKE": "Delegate take configuration" + "TAKE": "Delegate take configuration", }, "SUBNETS": { "INFO": "Subnet Information", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 9f359e92..347b9f09 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,8 +1,12 @@ from dataclasses import dataclass +from enum import Enum from typing import Optional, Any, Union import bt_decode import netaddr +from scalecodec import ScaleBytes +from scalecodec.base import RuntimeConfiguration +from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode from bittensor_cli.src.bittensor.balances import Balance @@ -10,6 +14,63 @@ from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float +class ChainDataType(Enum): + NeuronInfo = 1 + SubnetInfoV2 = 2 + DelegateInfo = 3 + NeuronInfoLite = 4 + DelegatedInfo = 5 + StakeInfo = 6 + IPInfo = 7 + SubnetHyperparameters = 8 + SubstakeElements = 9 + DynamicPoolInfoV2 = 10 + DelegateInfoLite = 11 + DynamicInfo = 12 + ScheduledColdkeySwapInfo = 13 + SubnetInfo = 14 + SubnetState = 15 + + +def from_scale_encoding_using_type_string( + input_: Union[list[int], bytes, ScaleBytes], type_string: str +) -> Optional[dict]: + if isinstance(input_, ScaleBytes): + as_scale_bytes = input_ + else: + if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): + vec_u8 = input_ + as_bytes = bytes(vec_u8) + elif isinstance(input_, bytes): + as_bytes = input_ + else: + raise TypeError("input must be a List[int], bytes, or ScaleBytes") + as_scale_bytes = ScaleBytes(as_bytes) + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) + return obj.decode() + + +def from_scale_encoding( + input_: Union[list[int], bytes, ScaleBytes], + type_name: ChainDataType, + is_vec: bool = False, + is_option: bool = False, +) -> Optional[dict]: + type_string = type_name.name + if type_name == ChainDataType.DelegatedInfo: + # DelegatedInfo is a tuple of (DelegateInfo, Compact) + type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" + if is_option: + type_string = f"Option<{type_string}>" + if is_vec: + type_string = f"Vec<{type_string}>" + + return from_scale_encoding_using_type_string(input_, type_string) + + def decode_account_id(account_id_bytes: tuple): # Convert the AccountId bytes to a Base64 string return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) @@ -729,6 +790,40 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfoV2"]: return decoded + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetInfoV2": + """Returns a SubnetInfoV2 object from a decoded SubnetInfoV2 dictionary.""" + # init dynamic pool object + pool_info = decoded["dynamic_pool"] + if pool_info: + pool = DynamicPool( + True, + pool_info["netuid"], + pool_info["alpha_issuance"], + pool_info["alpha_outstanding"], + pool_info["alpha_reserve"], + pool_info["tao_reserve"], + pool_info["k"], + ) + else: + pool = DynamicPool(False, decoded["netuid"], 0, 0, 0, 0, 0) + + return SubnetInfoV2( + netuid=decoded["netuid"], + owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), + max_allowed_validators=decoded["max_allowed_validators"], + scaling_law_power=decoded["scaling_law_power"], + subnetwork_n=decoded["subnetwork_n"], + max_n=decoded["max_allowed_uids"], + blocks_since_epoch=decoded["blocks_since_last_step"], + modality=decoded["network_modality"], + emission_value=decoded["emission_values"], + burn=Balance.from_rao(decoded["burn"]), + tao_locked=Balance.from_rao(decoded["tao_locked"]), + hyperparameters=decoded["hyperparameters"], + dynamic_pool=pool, + ) + @dataclass class DynamicInfo: From fc2bbd24f999cdbdb2c144fbaaf74cf5da46c986 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 23 Oct 2024 16:49:46 -0700 Subject: [PATCH 044/332] btcli s metagraph (awaiting confirmation for few fields) --- bittensor_cli/cli.py | 36 ++++++-- bittensor_cli/src/__init__.py | 4 +- bittensor_cli/src/bittensor/minigraph.py | 50 +++++++++-- bittensor_cli/src/commands/subnets.py | 110 +++++++++++++++++------ 4 files changed, 157 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d8065829..2b0e6163 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -472,7 +472,9 @@ def __init__(self): "use_cache": True, "metagraph_cols": { "UID": True, - "STAKE": True, + "GLOBAL_STAKE": True, + "LOCAL_STAKE": True, + "STAKE_WEIGHT": True, "RANK": True, "TRUST": True, "CONSENSUS": True, @@ -838,15 +840,33 @@ def main_callback( """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be overriden by passing them explicitly in the command line. """ - # create config file if it does not exist - if not os.path.exists(self.config_path): + + # Load or create the config file + if os.path.exists(self.config_path): + with open(self.config_path, "r") as f: + config = safe_load(f) + else: directory_path = Path(self.config_base_path) directory_path.mkdir(exist_ok=True, parents=True) - with open(self.config_path, "w+") as f: - safe_dump(defaults.config.dictionary, f) - # check config - with open(self.config_path, "r") as f: - config = safe_load(f) + config = defaults.config.dictionary.copy() + with open(self.config_path, "w") as f: + safe_dump(config, f) + + # Update missing values + updated = False + for key, value in defaults.config.dictionary.items(): + if key not in config: + config[key] = value + updated = True + elif isinstance(value, dict): + for sub_key, sub_value in value.items(): + if sub_key not in config[key]: + config[key][sub_key] = sub_value + updated = True + if updated: + with open(self.config_path, "w") as f: + safe_dump(config, f) + for k, v in config.items(): if k in self.config.keys(): self.config[k] = v diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 20cb5652..a4c749fd 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -77,7 +77,9 @@ class config: "use_cache": True, "metagraph_cols": { "UID": True, - "STAKE": True, + "GLOBAL_STAKE": True, + "LOCAL_STAKE": True, + "STAKE_WEIGHT": True, "RANK": True, "TRUST": True, "CONSENSUS": True, diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index 3f2aac0f..3d652d6d 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -3,7 +3,7 @@ import numpy as np from numpy.typing import NDArray -from bittensor_cli.src.bittensor.chain_data import NeuronInfo +from bittensor_cli.src.bittensor.chain_data import NeuronInfo, SubnetState from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( convert_root_weight_uids_and_vals_to_tensor, @@ -18,6 +18,7 @@ def __init__( netuid: int, neurons: list[NeuronInfo], subtensor: "SubtensorInterface", + subnet_state: "SubnetState", block: int, ): self.neurons = neurons @@ -62,12 +63,14 @@ def __init__( self.validator_trust = self._create_tensor( [neuron.validator_trust for neuron in self.neurons], dtype=np.float32 ) - self.total_stake = self._create_tensor( - [neuron.total_stake.tao for neuron in self.neurons], dtype=np.float32 - ) - self.stake = self._create_tensor( - [neuron.stake for neuron in self.neurons], dtype=np.float32 + + # Fetch stakes from subnet_state until we get updated data in NeuronInfo + global_stake_list, local_stake_list, stake_weights_list = self._process_stakes( + neurons, subnet_state ) + self.global_stake = self._create_tensor(global_stake_list, dtype=np.float32) + self.local_stake = self._create_tensor(local_stake_list, dtype=np.float32) + self.stake_weights = self._create_tensor(stake_weights_list, dtype=np.float32) async def __aenter__(self): if not self.weights: @@ -120,6 +123,41 @@ async def _set_weights_and_bonds(self): [neuron.bonds for neuron in self.neurons], "bonds" ) + def _process_stakes( + self, + neurons: list[NeuronInfo], + subnet_state: SubnetState, + ) -> tuple[list[float], list[float], list[float]]: + """ + Processes the global_stake, local_stake, and stake_weights based on the neuron's hotkey. + + Args: + neurons (List[NeuronInfo]): List of neurons. + subnet_state (SubnetState): The subnet state containing stake information. + + Returns: + tuple[list[float], list[float], list[float]]: Lists of global_stake, local_stake, and stake_weights. + """ + global_stake_list = [] + local_stake_list = [] + stake_weights_list = [] + hotkey_to_index = { + hotkey: idx for idx, hotkey in enumerate(subnet_state.hotkeys) + } + + for neuron in neurons: + idx = hotkey_to_index.get(neuron.hotkey) + if idx is not None: + global_stake_list.append(subnet_state.global_stake[idx].tao) + local_stake_list.append(subnet_state.local_stake[idx].tao) + stake_weights_list.append(subnet_state.stake_weight[idx]) + else: + global_stake_list.append(0.0) + local_stake_list.append(0.0) + stake_weights_list.append(0.0) + + return global_stake_list, local_stake_list, stake_weights_list + def _process_weights_or_bonds(self, data, attribute: str) -> NDArray: """ Processes the raw weights or bonds data and converts it into a structured tensor format. This method handles diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 7e539a90..75579694 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -752,6 +752,7 @@ async def register( ) +# TODO: Confirm emissions, incentive, Dividends are to be fetched from subnet_state or keep NeuronInfo async def metagraph_cmd( subtensor: Optional["SubtensorInterface"], netuid: Optional[int], @@ -788,15 +789,27 @@ async def metagraph_cmd( ), subtensor.substrate.get_block_number(block_hash=block_hash), ) + subnet_state: "SubnetState" = SubnetState.from_vec_u8( + ( + await subtensor.substrate.rpc_request( + method="subnetInfo_getSubnetState", params=[netuid, None] + ) + )["result"] + ) difficulty = int(difficulty_) total_issuance = Balance.from_rao(total_issuance_) metagraph = MiniGraph( - netuid=netuid, neurons=neurons, subtensor=subtensor, block=block + netuid=netuid, + neurons=neurons, + subtensor=subtensor, + subnet_state=subnet_state, + block=block, ) table_data = [] db_table = [] - total_stake = 0.0 + total_global_stake = 0.0 + total_local_stake = 0.0 total_rank = 0.0 total_validator_trust = 0.0 total_trust = 0.0 @@ -809,7 +822,9 @@ async def metagraph_cmd( ep = metagraph.axons[uid] row = [ str(neuron.uid), - "{:.5f}".format(metagraph.total_stake[uid]), + "{:.4f}".format(metagraph.global_stake[uid]), + "{:.4f}".format(metagraph.local_stake[uid]), + "{:.4f}".format(metagraph.stake_weights[uid]), "{:.5f}".format(metagraph.ranks[uid]), "{:.5f}".format(metagraph.trust[uid]), "{:.5f}".format(metagraph.consensus[uid]), @@ -830,7 +845,9 @@ async def metagraph_cmd( ] db_row = [ neuron.uid, - float(metagraph.total_stake[uid]), + float(metagraph.global_stake[uid]), + float(metagraph.local_stake[uid]), + float(metagraph.stake_weights[uid]), float(metagraph.ranks[uid]), float(metagraph.trust[uid]), float(metagraph.consensus[uid]), @@ -846,7 +863,8 @@ async def metagraph_cmd( ep.coldkey[:10], ] db_table.append(db_row) - total_stake += metagraph.total_stake[uid] + total_global_stake += metagraph.global_stake[uid] + total_local_stake += metagraph.local_stake[uid] total_rank += metagraph.ranks[uid] total_validator_trust += metagraph.validator_trust[uid] total_trust += metagraph.trust[uid] @@ -856,8 +874,9 @@ async def metagraph_cmd( total_emission += int(metagraph.emission[uid] * 1000000000) table_data.append(row) metadata_info = { - "stake": str(Balance.from_tao(total_stake)), - "total_stake": "\u03c4{:.5f}".format(total_stake), + "total_global_stake": "\u03c4 {:.5f}".format(total_global_stake), + "total_local_stake": f"{Balance.get_unit(netuid)} " + + "{:.5f}".format(total_local_stake), "rank": "{:.5f}".format(total_rank), "validator_trust": "{:.5f}".format(total_validator_trust), "trust": "{:.5f}".format(total_trust), @@ -881,7 +900,9 @@ async def metagraph_cmd( "metagraph", columns=[ ("UID", "INTEGER"), - ("STAKE", "REAL"), + ("GLOBAL_STAKE", "REAL"), + ("LOCAL_STAKE", "REAL"), + ("STAKE_WEIGHT", "REAL"), ("RANK", "REAL"), ("TRUST", "REAL"), ("CONSENSUS", "REAL"), @@ -925,11 +946,23 @@ async def metagraph_cmd( columns=[ {"title": "UID", "field": "UID"}, { - "title": "Stake", - "field": "STAKE", + "title": "Global Stake", + "field": "GLOBAL_STAKE", "formatter": "money", "formatterParams": {"symbol": "τ", "precision": 5}, }, + { + "title": "Local Stake", + "field": "LOCAL_STAKE", + "formatter": "money", + "formatterParams": {"symbol": f"{Balance.get_unit(netuid)}", "precision": 5}, + }, + { + "title": "Stake Weight", + "field": "STAKE_WEIGHT", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, { "title": "Rank", "field": "RANK", @@ -993,19 +1026,40 @@ async def metagraph_cmd( ratio=0.75, ), ), - "STAKE": ( + "GLOBAL_STAKE": ( 1, Column( - "[bold white]STAKE(\u03c4)", - footer=metadata_info["total_stake"], + "[bold white]GLOBAL STAKE(\u03c4)", + footer=metadata_info["total_global_stake"], style="bright_cyan", justify="right", no_wrap=True, + ratio=1.6, + ), + ), + "LOCAL_STAKE": ( + 2, + Column( + f"[bold white]LOCAL STAKE({Balance.get_unit(netuid)})", + footer=metadata_info["total_local_stake"], + style="bright_green", + justify="right", + no_wrap=True, ratio=1.5, ), ), + "STAKE_WEIGHT": ( + 3, + Column( + f"[bold white]WEIGHT (\u03c4x{Balance.get_unit(netuid)})", + style="purple", + justify="right", + no_wrap=True, + ratio=1.3, + ), + ), "RANK": ( - 2, + 4, Column( "[bold white]RANK", footer=metadata_info["rank"], @@ -1016,7 +1070,7 @@ async def metagraph_cmd( ), ), "TRUST": ( - 3, + 5, Column( "[bold white]TRUST", footer=metadata_info["trust"], @@ -1027,7 +1081,7 @@ async def metagraph_cmd( ), ), "CONSENSUS": ( - 4, + 6, Column( "[bold white]CONSENSUS", footer=metadata_info["consensus"], @@ -1038,7 +1092,7 @@ async def metagraph_cmd( ), ), "INCENTIVE": ( - 5, + 7, Column( "[bold white]INCENTIVE", footer=metadata_info["incentive"], @@ -1049,7 +1103,7 @@ async def metagraph_cmd( ), ), "DIVIDENDS": ( - 6, + 8, Column( "[bold white]DIVIDENDS", footer=metadata_info["dividends"], @@ -1060,7 +1114,7 @@ async def metagraph_cmd( ), ), "EMISSION": ( - 7, + 9, Column( "[bold white]EMISSION(\u03c1)", footer=metadata_info["emission"], @@ -1071,7 +1125,7 @@ async def metagraph_cmd( ), ), "VTRUST": ( - 8, + 10, Column( "[bold white]VTRUST", footer=metadata_info["validator_trust"], @@ -1082,21 +1136,21 @@ async def metagraph_cmd( ), ), "VAL": ( - 9, + 11, Column( "[bold white]VAL", justify="center", style="bright_white", no_wrap=True, - ratio=0.4, + ratio=0.7, ), ), "UPDATED": ( - 10, + 12, Column("[bold white]UPDATED", justify="right", no_wrap=True, ratio=1), ), "ACTIVE": ( - 11, + 13, Column( "[bold white]ACTIVE", justify="center", @@ -1106,7 +1160,7 @@ async def metagraph_cmd( ), ), "AXON": ( - 12, + 14, Column( "[bold white]AXON", justify="left", @@ -1116,7 +1170,7 @@ async def metagraph_cmd( ), ), "HOTKEY": ( - 13, + 15, Column( "[bold white]HOTKEY", justify="center", @@ -1126,7 +1180,7 @@ async def metagraph_cmd( ), ), "COLDKEY": ( - 14, + 16, Column( "[bold white]COLDKEY", justify="center", @@ -1159,7 +1213,7 @@ async def metagraph_cmd( f"Net: [bright_cyan]{metadata_info['net']}[/bright_cyan], " f"Block: [bright_cyan]{metadata_info['block']}[/bright_cyan], " f"N: [bright_green]{metadata_info['N0']}[/bright_green]/[bright_red]{metadata_info['N1']}[/bright_red], " - f"Stake: [dark_orange]{metadata_info['stake']}[/dark_orange], " + f"Total Local Stake: [dark_orange]{metadata_info['total_local_stake']}[/dark_orange], " f"Issuance: [bright_blue]{metadata_info['issuance']}[/bright_blue], " f"Difficulty: [bright_cyan]{metadata_info['difficulty']}[/bright_cyan]\n" ), From 83690256f3a0c282c4d37e2c3b81ba5665041b1b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 23 Oct 2024 17:20:18 -0700 Subject: [PATCH 045/332] Fixed mandatory netuid + help for s show --- bittensor_cli/cli.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2b0e6163..c03c1ee4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -159,12 +159,10 @@ class Options: "-n", help="Set the netuid(s) to exclude. Separate multiple netuids with a comma, for example: `-n 0,1,2`.", ) - netuid = ( - typer.Option( - None, - help="The netuid of the subnet in the root network, (e.g. 1).", - prompt=True, - ), + netuid = typer.Option( + None, + help="The netuid of the subnet in the root network, (e.g. 1).", + prompt=True, ) netuid_not_req = typer.Option( None, @@ -710,6 +708,9 @@ def __init__(self): self.subnets_app.command( "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_metagraph) + self.subnets_app.command( + "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + )(self.subnets_show) # weights commands self.weights_app.command( @@ -3354,6 +3355,13 @@ def subnets_show( verbose: bool = Options.verbose, prompt: bool = Options.prompt, ): + """ + Displays detailed information about a subnet including participants and their state. + + EXAMPLE + + [green]$[/green] btcli subnets list + """ self.verbosity_handler(quiet, verbose) subtensor = self.initialize_chain(network) return self._run_command( From bb508a540024db8c3ed3e501c93af0e7122df37d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 23 Oct 2024 22:47:15 -0700 Subject: [PATCH 046/332] btcli st list fixed --- bittensor_cli/cli.py | 3 ++ bittensor_cli/src/bittensor/chain_data.py | 5 ++ bittensor_cli/src/commands/stake/stake.py | 64 +++++++++++------------ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c03c1ee4..ec75bcba 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -645,6 +645,9 @@ def __init__(self): self.stake_app.command( "remove", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_remove) + self.stake_app.command( + "list", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.stake_list) # stake-children commands children_app = typer.Typer() diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 347b9f09..c2236119 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1462,7 +1462,12 @@ def decode(result: list[int]) -> list[dict]: "type_mapping": [ ["hotkey", "AccountId"], ["coldkey", "AccountId"], + ["netuid", "Compact"], ["stake", "Compact"], + ["locked", "Compact"], + ["emission", "Compact"], + ["drain", "Compact"], + ["is_registered", "bool"], ], }, "DynamicInfo": { diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 541fcded..e7bff212 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1612,40 +1612,40 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): hotkeys_to_substakes[hotkey] = [] hotkeys_to_substakes[hotkey].append(substake) - # Iterate over each hotkey and make a table - all_hotkeys_total_global_tao = Balance(0) - all_hotkeys_total_tao_value = Balance(0) - for hotkey in hotkeys_to_substakes.keys(): - stake, value = table_substakes(hotkey, hotkeys_to_substakes[hotkey]) - all_hotkeys_total_global_tao += stake - all_hotkeys_total_tao_value += value - - console.print("\n\n") - console.print( - f"Wallet:\n" - f" Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n" - f" Free Balance: [aquamarine3]{balance}[/aquamarine3]\n" - f" Total TAO ({Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n" - f" Total Value ({Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]" - ) - console.print( - """ + # Iterate over each hotkey and make a table + all_hotkeys_total_global_tao = Balance(0) + all_hotkeys_total_tao_value = Balance(0) + for hotkey in hotkeys_to_substakes.keys(): + stake, value = table_substakes(hotkey, hotkeys_to_substakes[hotkey]) + all_hotkeys_total_global_tao += stake + all_hotkeys_total_tao_value += value + + console.print("\n\n") + console.print( + f"Wallet:\n" + f" Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n" + f" Free Balance: [aquamarine3]{balance}[/aquamarine3]\n" + f" Total TAO ({Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n" + f" Total Value ({Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]" + ) + console.print( + """ [bold white]Description[/bold white]: - Each table displays information about your coldkey's staking accounts with a hotkey. - The header of the table displays the hotkey and the footer displays the total stake and total value of all your staking accounts. - The columns of the table are as follows: - - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). - - [bold white]Symbol[/bold white]: The symbol representing the subnet stake's unit. - - [bold white]TAO[/bold white]: The hotkey's TAO balance on this subnet. This is this hotkey's proportion of total TAO staked into the subnet divided by the hotkey's share of outstanding stake. - - [bold white]Stake[/bold white]: The hotkey's stake balance in subnets staking unit. - - [bold white]Rate[/bold white]: The rate of exchange between the subnet's staking unit and the subnet's TAO. - - [bold white]Value[/bold white]: The price of the hotkey's stake in TAO computed via the exchange rate. - - [bold white]Swap[/bold white]: The amount of TAO received when unstaking all of the hotkey's stake (with slippage). - - [bold white]Registered[/bold white]: Whether the hotkey is registered on this subnet. - - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. - - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). +Each table displays information about your coldkey's staking accounts with a hotkey. +The header of the table displays the hotkey and the footer displays the total stake and total value of all your staking accounts. +The columns of the table are as follows: + - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). + - [bold white]Symbol[/bold white]: The symbol representing the subnet stake's unit. + - [bold white]TAO[/bold white]: The hotkey's TAO balance on this subnet. This is this hotkey's proportion of total TAO staked into the subnet divided by the hotkey's share of outstanding stake. + - [bold white]Stake[/bold white]: The hotkey's stake balance in subnets staking unit. + - [bold white]Rate[/bold white]: The rate of exchange between the subnet's staking unit and the subnet's TAO. + - [bold white]Value[/bold white]: The price of the hotkey's stake in TAO computed via the exchange rate. + - [bold white]Swap[/bold white]: The amount of TAO received when unstaking all of the hotkey's stake (with slippage). + - [bold white]Registered[/bold white]: Whether the hotkey is registered on this subnet. + - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. + - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). """ - ) + ) async def move_stake( From f8e51aa1e4e8676150bb07308bbcb78e8acee14a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 23 Oct 2024 23:17:36 -0700 Subject: [PATCH 047/332] st move [wip] --- bittensor_cli/cli.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ec75bcba..da78ce70 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -648,6 +648,9 @@ def __init__(self): self.stake_app.command( "list", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_list) + self.stake_app.command( + "move", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.stake_move) # stake-children commands children_app = typer.Typer() @@ -2750,22 +2753,36 @@ def stake_move( wallet_hotkey=Options.wallet_hotkey, origin_netuid: int = typer.Option(help="Origin netuid", prompt=True), destination_netuid: int = typer.Option(help="Destination netuid", prompt=True), - destination_hotkey: str = typer.Option( # TODO also accept name - help="Destination hotkey", prompt=True + destination_hotkey: Optional[str] = typer.Option( + None, help="Destination hotkey", prompt=False + ), + amount: float = typer.Option( + 0.0, + "--amount", + help="The amount of TAO to stake", + prompt=True, ), - amount: Optional[float] = typer.Option(help="Amount", prompt=False), stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), prompt: bool = Options.prompt, ): + # TODO: Improve logic of moving stake (dest hotkey) + ask_for = ( + [WO.NAME, WO.PATH] if destination_hotkey else [WO.NAME, WO.HOTKEY, WO.PATH] + ) + validate = WV.WALLET if destination_hotkey else WV.WALLET_AND_HOTKEY + wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME], - validate=WV.WALLET_AND_HOTKEY, + ask_for=ask_for, + validate=validate, ) + if not destination_hotkey: + destination_hotkey = wallet.hotkey.ss58_address + return self._run_command( stake.move_stake( subtensor=self.initialize_chain(network), From 1ed2bb0a36064a3db89eb3bc0d303a8dfe157967 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 15:53:29 +0200 Subject: [PATCH 048/332] Improve error handling. --- bittensor_cli/cli.py | 2 +- .../src/bittensor/subtensor_interface.py | 6 +++--- bittensor_cli/src/commands/stake/stake.py | 18 ++++++++++++++---- bittensor_cli/src/commands/subnets.py | 5 ++++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index da78ce70..d8bd7d81 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -290,7 +290,7 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in if netuid is None and all_netuids is True: return None elif netuid is None and all_netuids is False: - return typer.prompt( + return Prompt.ask( "Enter the netuid to use. Leave blank for all netuids.", default=None, show_default=False, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 74fd7a78..e1b9fee6 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1244,14 +1244,14 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( results: dict[str, dict[int, "Balance"]] = { hk_ss58: {} for hk_ss58 in hotkey_ss58s } - for idx, item in enumerate(batch_call): + for idx, (_, val) in enumerate(batch_call): hotkey_idx = idx // len(netuids) netuid_idx = idx % len(netuids) hotkey_ss58 = hotkey_ss58s[hotkey_idx] netuid = netuids[netuid_idx] value = ( - Balance.from_rao(item).set_unit(netuid) - if item is not None + Balance.from_rao(val).set_unit(netuid) + if val is not None else Balance(0).set_unit(netuid) ) results[hotkey_ss58][netuid] = value diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index e7bff212..21d42345 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -11,6 +11,7 @@ from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Table, Column import typer +from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo @@ -1069,6 +1070,9 @@ async def stake_add( return False async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" + ) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -1081,9 +1085,15 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + except SubstrateRequestException as e: + err_console.print( + f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + ) + return if not prompt: # TODO verbose? console.print( f":white_heavy_check_mark: [green]Submitted {amount_} to {netuid_i}[/green]" @@ -1092,7 +1102,7 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): await response.process_events() if not await response.is_success: err_console.print( - f":cross_mark: [red]Failed[/red] with error: {response.error_message}" + f"\n{failure_prelude} with error: {response.error_message}" ) else: new_balance_, new_stake_ = await asyncio.gather( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 75579694..6a21c09f 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -955,7 +955,10 @@ async def metagraph_cmd( "title": "Local Stake", "field": "LOCAL_STAKE", "formatter": "money", - "formatterParams": {"symbol": f"{Balance.get_unit(netuid)}", "precision": 5}, + "formatterParams": { + "symbol": f"{Balance.get_unit(netuid)}", + "precision": 5, + }, }, { "title": "Stake Weight", From b2bd25cbdc484911b1f78b823dc2c0a428a5dff5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 17:34:01 +0200 Subject: [PATCH 049/332] [WIP] Stake Add --- bittensor_cli/src/commands/stake/stake.py | 50 ++++++++++++++--------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 21d42345..6a783aab 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -3,6 +3,7 @@ import json import sqlite3 from contextlib import suppress +from functools import partial from typing import TYPE_CHECKING, Optional, Sequence, Union, cast @@ -1069,7 +1070,10 @@ async def stake_add( if not Confirm.ask("Would you like to continue?"): return False - async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): + async def send_extrinsic( + netuid_i, amount_, current, staking_address_ss58, status=None + ): + err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) @@ -1090,7 +1094,7 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) except SubstrateRequestException as e: - err_console.print( + err_out( f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" ) return @@ -1101,8 +1105,8 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): else: await response.process_events() if not await response.is_success: - err_console.print( - f"\n{failure_prelude} with error: {response.error_message}" + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: new_balance_, new_stake_ = await asyncio.gather( @@ -1124,23 +1128,31 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): # Perform staking operation. wallet.unlock_coldkey() - with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): - extrinsics_coroutines = [ - send_extrinsic(ni, am, curr, staking_address) - for (ni, am, curr) in zip( - netuids, stake_amount_balance, current_stake_balances - ) - for _, staking_address in hotkeys_to_stake_to - ] - if len(extrinsics_coroutines) == 1: - await asyncio.gather(*extrinsics_coroutines) - else: + extrinsics_coroutines = [ + send_extrinsic(ni, am, curr, staking_address) + for i, (ni, am, curr) in enumerate( + zip(netuids, stake_amount_balance, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + ] + if len(extrinsics_coroutines) == 1: + with console.status( + f"\n:satellite: Staking on netuid(s): {netuids} ..." + ) as status: + await extrinsics_coroutines[0] + else: + with console.status(":satellite: Checking transaction rate limit ..."): tx_rate_limit_blocks = await subtensor.substrate.query( module="SubtensorModule", storage_function="TxRateLimit" ) - if tx_rate_limit_blocks > 0: - for item in extrinsics_coroutines: - await item + netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] + for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs): + ni, hk = kp + with console.status( + f"\n:satellite: Staking on netuid {ni} with hotkey {hk}... ..." + ): + await item + if tx_rate_limit_blocks > 0: with console.status( f":hourglass: [yellow]Waiting for tx rate limit:" f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" @@ -1148,8 +1160,6 @@ async def send_extrinsic(netuid_i, amount_, current, staking_address_ss58): await asyncio.sleep( tx_rate_limit_blocks * 12 ) # 12 sec per block - else: - await asyncio.gather(*extrinsics_coroutines) async def unstake( From 1ced08dec514d3da4f5860e894e28287cf15a314 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 24 Oct 2024 09:39:02 -0700 Subject: [PATCH 050/332] fix netuid from str to int --- bittensor_cli/src/commands/stake/stake.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 6a783aab..b47bd931 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1177,7 +1177,7 @@ async def unstake( ): """Unstake token of amount from hotkey(s).""" netuids = ( - [netuid] if netuid is not None else await subtensor.get_all_subnet_netuids() + [int(netuid)] if netuid is not None else await subtensor.get_all_subnet_netuids() ) # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] @@ -1352,7 +1352,7 @@ async def unstake( f"Amount ({Balance.get_unit(1)})", justify="center", style="dark_sea_green" ) table.add_column( - f"Rate ({Balance.get_unit(0)}/{bt.Balance.get_unit(1)})", + f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", justify="center", style="light_goldenrod2", ) @@ -1365,7 +1365,7 @@ async def unstake( table.add_column("Slippage", justify="center", style="rgb(220,50,47)") for row in rows: table.add_row(*row) - bt.__console__.print(table) + console.print(table) message = "" if max_float_slippage > 5: message += f"-------------------------------------------------------------------------------------------------------------------\n" From f682bdddfcca416bfa85683ca189d4268edb1b21 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 24 Oct 2024 09:49:12 -0700 Subject: [PATCH 051/332] for `stake_add` fix netuid from str to int --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b47bd931..90eee19c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -860,7 +860,7 @@ async def stake_add( """ netuids = ( - [netuid] if netuid is not None else await subtensor.get_all_subnet_netuids() + [int(netuid)] if netuid is not None else await subtensor.get_all_subnet_netuids() ) # Init the table. table = Table( From b3e774cf703d89ff80ccf1a22facffd1af8e21b7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 18:56:07 +0200 Subject: [PATCH 052/332] Convert netuid ask to int. --- bittensor_cli/cli.py | 6 +++++- bittensor_cli/src/commands/stake/stake.py | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d8bd7d81..b158692f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -290,11 +290,15 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in if netuid is None and all_netuids is True: return None elif netuid is None and all_netuids is False: - return Prompt.ask( + answer = Prompt.ask( "Enter the netuid to use. Leave blank for all netuids.", default=None, show_default=False, ) + if answer is None: + return None + else: + return int(answer) else: return netuid diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 6a783aab..7f4f16dc 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1138,7 +1138,7 @@ async def send_extrinsic( if len(extrinsics_coroutines) == 1: with console.status( f"\n:satellite: Staking on netuid(s): {netuids} ..." - ) as status: + ): await extrinsics_coroutines[0] else: with console.status(":satellite: Checking transaction rate limit ..."): @@ -1152,14 +1152,14 @@ async def send_extrinsic( f"\n:satellite: Staking on netuid {ni} with hotkey {hk}... ..." ): await item - if tx_rate_limit_blocks > 0: - with console.status( - f":hourglass: [yellow]Waiting for tx rate limit:" - f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ): - await asyncio.sleep( - tx_rate_limit_blocks * 12 - ) # 12 sec per block + if tx_rate_limit_blocks > 0: + with console.status( + f":hourglass: [yellow]Waiting for tx rate limit:" + f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" + ): + await asyncio.sleep( + tx_rate_limit_blocks * 12 + ) # 12 sec per block async def unstake( From 3470d3ace9f48160f680f2176dcdfa45ea609e8f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 19:19:56 +0200 Subject: [PATCH 053/332] Correctly use staking address in unstake. --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index c35bfa80..dfdefb09 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1272,7 +1272,7 @@ async def unstake( current_stake_balance: Balance = ( await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58_address, + hotkey_ss58=staking_address_ss58, netuid=netuid, ) ) From a2e30073845d7a9ddf2af9466f4b07e748a3d006 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 19:50:52 +0200 Subject: [PATCH 054/332] Correctly use staking address for name in all parts of unstake. --- bittensor_cli/src/commands/stake/stake.py | 104 +++++++++++----------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index dfdefb09..59760ac5 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -860,7 +860,9 @@ async def stake_add( """ netuids = ( - [int(netuid)] if netuid is not None else await subtensor.get_all_subnet_netuids() + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() ) # Init the table. table = Table( @@ -1136,9 +1138,7 @@ async def send_extrinsic( for _, staking_address in hotkeys_to_stake_to ] if len(extrinsics_coroutines) == 1: - with console.status( - f"\n:satellite: Staking on netuid(s): {netuids} ..." - ): + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): await extrinsics_coroutines[0] else: with console.status(":satellite: Checking transaction rate limit ..."): @@ -1157,9 +1157,7 @@ async def send_extrinsic( f":hourglass: [yellow]Waiting for tx rate limit:" f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" ): - await asyncio.sleep( - tx_rate_limit_blocks * 12 - ) # 12 sec per block + await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block async def unstake( @@ -1177,7 +1175,9 @@ async def unstake( ): """Unstake token of amount from hotkey(s).""" netuids = ( - [int(netuid)] if netuid is not None else await subtensor.get_all_subnet_netuids() + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() ) # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] @@ -1394,51 +1394,53 @@ async def unstake( with console.status( f"\n:satellite: Unstaking {amount_to_unstake_as_balance} from {staking_address_name} on netuid: {netuid} ..." ): - for netuid_i, amount, current in list( - zip(non_zero_netuids, unstake_amount_balance, current_stake_balances) - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58_address, - "netuid": netuid_i, - "amount_unstaked": amount.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - else: - await response.process_events() - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(response.error_message, subtensor.substrate)}" - ) + for hotkey in hotkeys_to_unstake_from: + staking_address_name, staking_address_ss58 = hotkey + for netuid_i, amount, current in list( + zip(non_zero_netuids, unstake_amount_balance, current_stake_balances) + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") else: - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58_address, - netuid=netuid_i, + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(response.error_message, subtensor.substrate)}" + ) + else: + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid_i, + ) + ).set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + console.print( + f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" ) - ).set_unit(netuid_i) - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - console.print( - f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): From 7f0b5ae60b0fb3cc145f0b5077af3367f5a1f020 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 11:26:00 -0700 Subject: [PATCH 055/332] Shows free balance in staking, fixes hotkey display in table --- bittensor_cli/cli.py | 31 +++++++++++++++++++---- bittensor_cli/src/commands/stake/stake.py | 2 +- bittensor_cli/src/commands/wallets.py | 1 + 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b158692f..9597336f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -291,7 +291,7 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - "Enter the netuid to use. Leave blank for all netuids.", + "[green]Enter the netuid to use. Leave blank for all netuids", default=None, show_default=False, ) @@ -2496,9 +2496,6 @@ def stake_add( ) raise typer.Exit() - if not stake_all and not amount and not max_stake: - amount = FloatPrompt.ask("[blue bold]Amount to stake (TAO τ)[/blue bold]") - if stake_all and not amount: if not Confirm.ask("Stake all the available TAO tokens?", default=False): raise typer.Exit() @@ -2518,7 +2515,7 @@ def stake_add( if not wallet_hotkey and not all_hotkeys and not include_hotkeys: hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [Press Enter to use config values]", ) if is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 @@ -2570,6 +2567,30 @@ def stake_add( else: excluded_hotkeys = [] + # TODO: Ask amount for each subnet explicitly if more than one + if not stake_all and not amount and not max_stake: + if netuid is not None: + free_balance, staked_balance = self._run_command( + wallets.wallet_balance( + wallet, self.initialize_chain(network), False, None + ) + ) + if free_balance == Balance.from_tao(0): + print_error("You dont have any balance to stake.") + raise typer.Exit() + amount = FloatPrompt.ask( + "[dark_orange]Amount to stake (TAO τ)[/dark_orange]" + ) + if Balance.from_tao(amount) > free_balance: + print_error( + f"You dont have enough balance to stake. Current free Balance: {free_balance}." + ) + raise typer.Exit() + else: + amount = FloatPrompt.ask( + "[dark_orange]Amount to stake to each netuid (TAO τ)[/dark_orange]" + ) + return self._run_command( stake.stake_add( wallet, diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 59760ac5..56268daa 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1022,7 +1022,7 @@ async def stake_add( ( str(netuid), # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", - f"{hotkey}", + f"{hotkey[1]}", str(amount_to_stake_as_balance), str(1 / float(dynamic_info.price)) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3faefd23..daf17c25 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -318,6 +318,7 @@ async def wallet_balance( ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() + return total_free_balance, total_staked_balance async def get_wallet_transfers(wallet_address: str) -> list[dict]: From d9ed6ad237918bf4b367df2dadbe74a7c583c11f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 11:26:12 -0700 Subject: [PATCH 056/332] ruff --- bittensor_cli/cli.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9597336f..c94b3661 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2569,27 +2569,27 @@ def stake_add( # TODO: Ask amount for each subnet explicitly if more than one if not stake_all and not amount and not max_stake: - if netuid is not None: - free_balance, staked_balance = self._run_command( - wallets.wallet_balance( - wallet, self.initialize_chain(network), False, None - ) + free_balance, staked_balance = self._run_command( + wallets.wallet_balance( + wallet, self.initialize_chain(network), False, None ) - if free_balance == Balance.from_tao(0): - print_error("You dont have any balance to stake.") - raise typer.Exit() + ) + if free_balance == Balance.from_tao(0): + print_error("You dont have any balance to stake.") + raise typer.Exit() + if netuid is not None: amount = FloatPrompt.ask( "[dark_orange]Amount to stake (TAO τ)[/dark_orange]" ) - if Balance.from_tao(amount) > free_balance: - print_error( - f"You dont have enough balance to stake. Current free Balance: {free_balance}." - ) - raise typer.Exit() else: amount = FloatPrompt.ask( "[dark_orange]Amount to stake to each netuid (TAO τ)[/dark_orange]" ) + if Balance.from_tao(amount) > free_balance: + print_error( + f"You dont have enough balance to stake. Current free Balance: {free_balance}." + ) + raise typer.Exit() return self._run_command( stake.stake_add( From cc201e0cc21bcc02164fa4331d735376a163ccfc Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 21:33:47 +0200 Subject: [PATCH 057/332] Changed get_all_subnet_dynamic_info to use Runtime API --- bittensor_cli/src/__init__.py | 4 ++++ bittensor_cli/src/bittensor/chain_data.py | 6 ++++-- bittensor_cli/src/bittensor/subtensor_interface.py | 11 ++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a4c749fd..3ab18fe2 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -278,6 +278,10 @@ class WalletValidationTypes(Enum): "params": [], "type": "Vec", }, + "get_all_dynamic_info": { + "params": [], + "type": "Vec", + }, } }, "SubnetRegistrationRuntimeApi": { diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index c2236119..bee31608 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -44,7 +44,9 @@ def from_scale_encoding_using_type_string( elif isinstance(input_, bytes): as_bytes = input_ else: - raise TypeError("input must be a List[int], bytes, or ScaleBytes") + raise TypeError( + f"input must be a list[int], bytes, or ScaleBytes, not {type(input_)}" + ) as_scale_bytes = ScaleBytes(as_bytes) rpc_runtime_config = RuntimeConfiguration() rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) @@ -853,7 +855,7 @@ def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: return DynamicInfo.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DynamicInfo"]: + def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo"]: decoded = from_scale_encoding( vec_u8, ChainDataType.DynamicInfo, is_vec=True, is_option=True ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e1b9fee6..73259ba9 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -385,15 +385,12 @@ async def get_total_stake_for_coldkey( sub_stakes = await self.get_stake_info_for_coldkeys( ss58_addresses, block_hash=block_hash ) - # Token pricing info dynamic_info = await self.get_all_subnet_dynamic_info() results = {} - for ss58, stake_info_list in sub_stakes.items(): all_staked_tao = 0 - for sub_stake in stake_info_list: if sub_stake.stake.rao == 0: continue @@ -417,7 +414,6 @@ async def get_total_stake_for_coldkey( all_staked_tao += tao_ownership.rao results[ss58] = Balance.from_rao(all_staked_tao) - return results async def get_total_stake_for_hotkey( @@ -1296,8 +1292,9 @@ async def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: - json = await self.substrate.rpc_request( - method="subnetInfo_getAllDynamicInfo", params=[None] + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_all_dynamic_info", ) - subnets = DynamicInfo.list_from_vec_u8(json["result"]) + subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) return subnets From 2580614e37ca3efdb5fa70a2b7d8971690649bc4 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 16:40:32 -0400 Subject: [PATCH 058/332] add runtime apis to reg --- bittensor_cli/src/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 3ab18fe2..32ca0e0f 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -278,10 +278,27 @@ class WalletValidationTypes(Enum): "params": [], "type": "Vec", }, + "get_subnet_info_v2": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + }, + "get_subnets_info_v2": { + "params": [], + "type": "Vec", + }, "get_all_dynamic_info": { "params": [], "type": "Vec", }, + "get_dynamic_info": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, } }, "SubnetRegistrationRuntimeApi": { From 80db62252635b9c23f8106f843d978087def55fa Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 16:43:01 -0400 Subject: [PATCH 059/332] use runtime api for neuron --- .../src/bittensor/subtensor_interface.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 73259ba9..02c3271f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -730,17 +730,23 @@ async def neuron_for_uid( """ if uid is None: return NeuronInfo.get_null_neuron() - - params = [netuid, uid, block_hash] if block_hash else [netuid, uid] - json_body = await self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method + + hex_bytes_result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], + block_hash=block_hash, ) - if not (result := json_body.get("result", None)): + + if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() - bytes_result = bytes(result) + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) + return NeuronInfo.from_vec_u8(bytes_result) async def get_delegated( From f3c7bd6dd845ca19a908b456566c73d01603228e Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 16:44:06 -0400 Subject: [PATCH 060/332] get delegated runtime api --- .../src/bittensor/subtensor_interface.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 02c3271f..de035864 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -775,15 +775,23 @@ async def get_delegated( else (self.substrate.last_block_hash if reuse_block else None) ) encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - json_body = await self.substrate.rpc_request( - method="delegateInfo_getDelegated", - params=([block_hash, encoded_coldkey] if block_hash else [encoded_coldkey]), + + hex_bytes_result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[encoded_coldkey], + block_hash=block_hash, ) - if not (result := json_body.get("result")): + if not (result := hex_bytes_result): return [] - return DelegateInfo.delegated_list_from_vec_u8(bytes(result)) + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) + + return DelegateInfo.delegated_list_from_vec_u8(bytes_result) async def query_identity( self, From e03d4796da9d45310f7af94a3166885cef16ea9c Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 16:45:26 -0400 Subject: [PATCH 061/332] add todo --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index de035864..e5d72647 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1167,7 +1167,7 @@ async def get_delegates_by_netuid_light( A list of DelegateInfo objects detailing each delegate's characteristics. """ - + # TODO (Ben): doesn't exist params = [netuid] if not block_hash else [netuid, block_hash] json_body = await self.substrate.rpc_request( method="delegateInfo_getDelegatesLight", # custom rpc method From 1368bd6f007596b0850dc72b83885871991ebe36 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Oct 2024 23:10:22 +0200 Subject: [PATCH 062/332] Update balance to show non-Tao on the right side of the number. --- bittensor_cli/src/bittensor/balances.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index bc935f01..4a795531 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -73,7 +73,10 @@ def __str__(self): """ Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. """ - return f"{self.unit}{float(self.tao):,.9f}" + if self.unit == UNITS[0]: + return f"{self.unit} {float(self.tao):,.4f}" + else: + return f"{float(self.tao):,.4f} {self.unit}\u200e" def __rich__(self): return "[green]{}[/green][green]{}[/green][green].[/green][dim green]{}[/dim green]".format( From eafad578b29fc7908d8878719592e012463e0360 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 17:42:41 -0400 Subject: [PATCH 063/332] use runtime api for get subnet dynamic info --- .../src/bittensor/subtensor_interface.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e5d72647..28561e54 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1184,10 +1184,22 @@ async def get_delegates_by_netuid_light( async def get_subnet_dynamic_info( self, netuid: int, block_hash: Optional[str] = None ) -> "DynamicInfo": - json = await self.substrate.rpc_request( - method="subnetInfo_getDynamicInfo", params=[netuid, block_hash] + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=block_hash, ) - subnets = DynamicInfo.from_vec_u8(json["result"]) + + if hex_bytes_result is None: + return None + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + subnets = DynamicInfo.from_vec_u8(bytes_result) return subnets async def get_stake_for_coldkey_and_hotkey_on_netuid( From 39fbaf242337dbe0b18bc6633097c3dbe2f6ccbc Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 17:44:43 -0400 Subject: [PATCH 064/332] replace with runtime api --- .../src/bittensor/extrinsics/registration.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 112ce4fd..86bad3c9 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -493,17 +493,23 @@ async def get_neuron_for_pubkey_and_subnet(): ) if uid is None: return NeuronInfo.get_null_neuron() + - params = [netuid, uid] - json_body = await subtensor.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, + hex_bytes_result = await subtensor.substrate.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], ) - if not (result := json_body.get("result", None)): + if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() + + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) - return NeuronInfo.from_vec_u8(bytes(result)) + return NeuronInfo.from_vec_u8(bytes_result) print_verbose("Checking subnet status") if not await subtensor.subnet_exists(netuid): From 292434d570080e101a5aa64ea8f30e3cfa1dd2e6 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 17:46:16 -0400 Subject: [PATCH 065/332] use runtime api here also --- bittensor_cli/src/commands/wallets.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index daf17c25..1b0fe474 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1114,17 +1114,12 @@ async def _fetch_neuron_for_netuid( """ async def neurons_lite_for_uid(uid: int) -> dict[Any, Any]: - call_definition = TYPE_REGISTRY["runtime_api"]["NeuronInfoRuntimeApi"][ - "methods" - ]["get_neurons_lite"] - data = await subtensor.encode_params( - call_definition=call_definition, params=[uid] - ) block_hash = subtensor.substrate.last_block_hash - hex_bytes_result = await subtensor.substrate.rpc_request( - method="state_call", - params=["NeuronInfoRuntimeApi_get_neurons_lite", data, block_hash], - reuse_block_hash=True, + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[uid], + block_hash=block_hash, ) return hex_bytes_result From 6087513c3c766ed00f3ece6999f3527abe7af88e Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 17:50:13 -0400 Subject: [PATCH 066/332] replace with runtime api --- bittensor_cli/src/commands/subnets.py | 57 +++++++++++++++++++-------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 6a21c09f..266f2fb7 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -269,16 +269,22 @@ async def subnets_list( async def show(subtensor: "SubtensorInterface", netuid: int, prompt: bool = True): async def show_root(): all_subnets = await subtensor.get_all_subnet_dynamic_info() - root_state: "SubnetState" = SubnetState.from_vec_u8( - ( - await subtensor.substrate.rpc_request( - method="subnetInfo_getSubnetState", params=[0, None] - ) - )["result"] + + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[0], ) - if root_state is None: + if (bytes_result := hex_bytes_result) is None: err_console.print("The root subnet does not exist") return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + + root_state: "SubnetState" = SubnetState.from_vec_u8( + bytes_result + ) if len(root_state.hotkeys) == 0: err_console.print( "The root-subnet is currently empty with 0 UIDs registered." @@ -373,12 +379,20 @@ async def show_root(): async def show_subnet(netuid_: int): subnet_info = await subtensor.get_subnet_dynamic_info(netuid_) + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid_], + ) + if (bytes_result := hex_bytes_result) is None: + err_console.print(f"Subnet {netuid_} does not exist") + return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + subnet_state: "SubnetState" = SubnetState.from_vec_u8( - ( - await subtensor.substrate.rpc_request( - method="subnetInfo_getSubnetState", params=[netuid_, None] - ) - )["result"] + bytes_result ) if subnet_info is None: err_console.print(f"Subnet {netuid_} does not exist") @@ -789,12 +803,21 @@ async def metagraph_cmd( ), subtensor.substrate.get_block_number(block_hash=block_hash), ) + + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid], + ) + if not (bytes_result := hex_bytes_result): + err_console.print(f"Subnet {netuid} does not exist") + return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + subnet_state: "SubnetState" = SubnetState.from_vec_u8( - ( - await subtensor.substrate.rpc_request( - method="subnetInfo_getSubnetState", params=[netuid, None] - ) - )["result"] + bytes_result ) difficulty = int(difficulty_) From 8642fa269faedc4d3bfbbb2ae4683208a2707242 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 00:04:14 +0200 Subject: [PATCH 067/332] Column footers --- bittensor_cli/src/commands/subnets.py | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 6a21c09f..05f12ab6 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -492,24 +492,28 @@ async def show_subnet(netuid_: int): for idx in range(len(subnet_state.emission)) ] ) + tao_sum = Balance(0) + stake_sum = Balance(0) for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum if emission_sum != 0 else 0 ) + tao_sum += subnet_state.global_stake[idx] + stake_sum += subnet_state.local_stake[idx] rows.append( ( - str(idx), - str(subnet_state.global_stake[idx]), - f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", - f"{subnet_state.stake_weight[idx]:.4f}", + str(idx), # UID + str(subnet_state.global_stake[idx]), # TAO + f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake + f"{subnet_state.stake_weight[idx]:.4f}", # Weight # str(subnet_state.dividends[idx]), - f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", - str(subnet_state.incentives[idx]), - f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", - f"{subnet_state.hotkeys[idx]}", - f"{subnet_state.coldkeys[idx]}", + f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", # Dividends + str(subnet_state.incentives[idx]), # Incentive + f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", # Emission + f"{subnet_state.hotkeys[idx]}", # Hotkey + f"{subnet_state.coldkeys[idx]}", # Coldkey ) ) # Add columns to the table @@ -519,12 +523,14 @@ async def show_subnet(netuid_: int): style="medium_purple", no_wrap=True, justify="right", + footer=str(tao_sum), ) table.add_column( f"Stake({Balance.get_unit(netuid_)})", style="green", no_wrap=True, justify="right", + footer=f"{stake_sum.set_unit(subnet_info.netuid)}", ) table.add_column( f"Weight({Balance.get_unit(0)}•{Balance.get_unit(netuid_)})", @@ -543,6 +549,7 @@ async def show_subnet(netuid_: int): style="aquamarine3", no_wrap=True, justify="center", + footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), ) table.add_column( "Hotkey", style="light_salmon3", no_wrap=True, justify="center" @@ -554,8 +561,8 @@ async def show_subnet(netuid_: int): table.add_row(*row) # Print the table - # bt.__console__.print("\n\n\n") - # bt.__console__.print(subnet_info_table) + # console.print("\n\n\n") + # console.print(subnet_info_table) console.print("\n\n") console.print(table) console.print("\n") From db27dc2bfa7dbe522ba74ef1ac33b78dcfc39203 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 19:57:30 -0400 Subject: [PATCH 068/332] add subnet state to reg --- bittensor_cli/src/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 32ca0e0f..9df23756 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -299,6 +299,10 @@ class WalletValidationTypes(Enum): "params": [{"name": "netuid", "type": "u16"}], "type": "Vec", }, + "get_subnet_state": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, } }, "SubnetRegistrationRuntimeApi": { From 0f13d4051bd5c10ab5bc8bd6c584d3c24ff46d73 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 17:11:35 -0700 Subject: [PATCH 069/332] Updated btcli s list --- bittensor_cli/cli.py | 9 ++ .../src/bittensor/subtensor_interface.py | 10 ++ bittensor_cli/src/commands/subnets.py | 110 +++++++----------- 3 files changed, 64 insertions(+), 65 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c94b3661..f20e82f4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2792,6 +2792,15 @@ def stake_move( ), prompt: bool = Options.prompt, ): + """ + Move Staked TAO to a hotkey from one subnet to another. + + THe move commands converts the origin subnet's dTao to Tao, and then converts Tao to destination subnet's dTao. + + EXAMPLE + + [green]$[/green] btcli stake move + """ # TODO: Improve logic of moving stake (dest hotkey) ask_for = ( [WO.NAME, WO.PATH] if destination_hotkey else [WO.NAME, WO.HOTKEY, WO.PATH] diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 73259ba9..da5fdf92 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -38,6 +38,7 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, + u64_normalized_float, ) @@ -1298,3 +1299,12 @@ async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: ) subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) return subnets + + async def get_global_weight(self, netuid: int, block_hash: Optional[str] = None): + global_weight = await self.substrate.query( + module="SubtensorModule", + storage_function="GlobalWeight", + params=[netuid], + block_hash=block_hash, + ) + return u64_normalized_float(global_weight) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 05f12ab6..aa4929a9 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -158,88 +158,66 @@ async def subnets_list( ): """List all subnet netuids in the network.""" # TODO add reuse-last and html-output and no-cache + async def fetch_global_weight(netuid): + try: + return netuid, await subtensor.get_global_weight(netuid) + except Exception as e: + print(f"Error fetching global weight for netuid {netuid}: {e}") + return netuid, None - # Initialize variables to store aggregated data rows = [] + subnets = await subtensor.get_all_subnet_dynamic_info() + netuids = [subnet.netuid for subnet in subnets] + + global_weight_tasks = [fetch_global_weight(netuid) for netuid in netuids] + global_weights = await asyncio.gather(*global_weight_tasks) + + global_weight_dict = {netuid: weight for netuid, weight in global_weights} for subnet in subnets: + netuid = subnet.netuid + global_weight = global_weight_dict.get(netuid) symbol = f"{subnet.symbol}\u200e" + + if netuid == 0: + emission_tao = 0.0 + else: + emission_tao = subnet.emission.tao + rows.append( ( - str(subnet.netuid), + str(netuid), f"[light_goldenrod1]{subnet.symbol}[light_goldenrod1]", - f"τ {subnet.emission.tao:.4f}", - # f"P( τ {subnet.tao_in.tao:,.4f},", + f"τ {emission_tao:.4f}", f"τ {subnet.tao_in.tao:,.4f}", - # f"{subnet.alpha_in.tao:,.4f} {subnet.symbol} )", f"{subnet.alpha_out.tao:,.4f} {symbol}", f"{subnet.price.tao:.4f} τ/{symbol}", - str(subnet.blocks_since_last_step) + "/" + str(subnet.tempo), - # f"{subnet.owner_locked}" + "/" + f"{subnet.total_locked}", - # f"{subnet.owner[:3]}...{subnet.owner[-3:]}", + f"{subnet.blocks_since_last_step}/{subnet.tempo}", + f"{global_weight:.4f}" if global_weight is not None else "N/A", ) ) + total_emissions = sum(float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0) - # TODO make this a reusable function - # Define table properties - console_width = console.width - 5 table = Table( - title="Subnet Info", - width=console_width, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"[underline dark_orange]Subnets[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, - ) - table.title = f"[white]Subnets - {subtensor.network}\n" - - # Add columns to the table - # price_total = f"τ{total_price.tao:.2f}/{bt.Balance.from_rao(dynamic_emission).tao:.2f}" - # above_price_threshold = total_price.tao > bt.Balance.from_rao(dynamic_emission).tao - - table.add_column("Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center") - table.add_column("Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center") - table.add_column( - f"Emission ({Balance.get_unit(0)})", - style="rgb(38,139,210)", - no_wrap=True, - justify="right", - ) - table.add_column( - f"TAO({Balance.get_unit(0)})", - style="medium_purple", - no_wrap=True, - justify="right", - ) - # table.add_column(f"{bt.Balance.get_unit(1)})", style="rgb(42,161,152)", no_wrap=True, justify="left") - table.add_column( - f"Stake({Balance.get_unit(1)})", style="green", no_wrap=True, justify="right" - ) - table.add_column( - f"Rate ({Balance.get_unit(1)}/{Balance.get_unit(0)})", - style="light_goldenrod2", - no_wrap=True, - justify="center", - ) - table.add_column( - "Tempo (k/n)", style="light_salmon3", no_wrap=True, justify="center" + show_lines=False, + pad_edge=True, ) - # table.add_column(f"Locked ({bt.Balance.get_unit(1)})", style="rgb(38,139,210)", no_wrap=True, justify="center") - # table.add_column("Owner", style="rgb(38,139,210)", no_wrap=True, justify="center") + + table.add_column("[bold white]NETUID", style="white", justify="center") + table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") + table.add_column(f"[bold white]EMISSION ({Balance.get_unit(0)})", style="light_goldenrod2", justify="right", footer=f"τ {total_emissions:.4f}") + table.add_column(f"[bold white]TAO ({Balance.get_unit(0)})", style="rgb(42,161,152)", justify="right") + table.add_column(f"[bold white]STAKE ({Balance.get_unit(1)})", style="light_salmon3", justify="right") + table.add_column(f"[bold white]RATE ({Balance.get_unit(1)}/{Balance.get_unit(0)})", style="medium_purple", justify="right") + table.add_column("[bold white]Tempo (k/n)", style="bright_magenta", justify="right", overflow="fold") + table.add_column("[bold white]Global weight (γ)", style="green", justify="right") # Sort rows by subnet.emission.tao, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) @@ -250,6 +228,8 @@ async def subnets_list( # Print the table console.print(table) + + # TODO: Add description for global weights console.print( """ [bold white]Description[/bold white]: @@ -288,12 +268,12 @@ async def show_root(): console_width = console.width - 5 table = Table( title="[white]Root Network", - width=console_width, + # width=console_width, safe_box=True, padding=(0, 1), collapse_padding=False, pad_edge=True, - expand=True, + # expand=True, show_header=True, show_footer=True, show_edge=False, From 7b78e19ef0cf34294449f0d7c942f3633dac8570 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 20:17:39 -0400 Subject: [PATCH 070/332] fix st list alignment --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 56268daa..668e85ab 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1478,7 +1478,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): for substake_ in substakes: netuid = substake_.netuid pool = dynamic_info[netuid] - symbol = f"{Balance.get_unit(netuid)}" + symbol = f"{Balance.get_unit(netuid)}\u200e" # TODO: what is this price var for? price = ( "{:.4f}{}".format( From d4978cff9b0935f4b9ea746e84ebbeacbafaa2f1 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 20:29:39 -0400 Subject: [PATCH 071/332] modify query_multiple --- bittensor_cli/src/bittensor/async_substrate_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 60ec9dce..a6baf541 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -4,7 +4,7 @@ from collections import defaultdict from dataclasses import dataclass from hashlib import blake2b -from typing import Optional, Any, Union, Callable, Awaitable, cast +from typing import Optional, Any, Union, Callable, Awaitable, cast, Iterable from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 from async_property import async_property @@ -1792,7 +1792,7 @@ async def query_multiple( runtime = await self.init_runtime(block_hash=block_hash) preprocessed: tuple[Preprocessed] = await asyncio.gather( *[ - self._preprocess([x], block_hash, storage_function, module) + self._preprocess([x] if not isinstance(x, Iterable) else list(x), block_hash, storage_function, module) for x in params ] ) From 877df05cbc4552bb2efce542dbeeca665b65c012 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 20:29:48 -0400 Subject: [PATCH 072/332] fix total stake for hotkey --- bittensor_cli/src/bittensor/subtensor_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 28561e54..5de331d5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -431,10 +431,11 @@ async def get_total_stake_for_hotkey( :return: {address: Balance objects} """ + netuids = await self.get_all_subnet_netuids(block_hash=block_hash) results = await self.substrate.query_multiple( - params=[s for s in ss58_addresses], + params=[p for p in zip(ss58_addresses, netuids)], module="SubtensorModule", - storage_function="TotalHotkeyStake", + storage_function="TotalHotkeyAlpha", block_hash=block_hash, reuse_block_hash=reuse_block, ) From 5b77906570313724a72f23c2ce90bbe8cad3d310 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 24 Oct 2024 20:34:33 -0400 Subject: [PATCH 073/332] unwrap tuple --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 5de331d5..500d281a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -432,14 +432,14 @@ async def get_total_stake_for_hotkey( :return: {address: Balance objects} """ netuids = await self.get_all_subnet_netuids(block_hash=block_hash) - results = await self.substrate.query_multiple( + results: dict[tuple[str, int], int] = await self.substrate.query_multiple( params=[p for p in zip(ss58_addresses, netuids)], module="SubtensorModule", storage_function="TotalHotkeyAlpha", block_hash=block_hash, reuse_block_hash=reuse_block, ) - return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} + return {k[0]: Balance.from_rao(r or 0) for (k, r) in results.items()} async def current_take( self, From 6c5de3cbbae3ce833746cb981b55959499e60fcc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 17:37:59 -0700 Subject: [PATCH 074/332] Improve fetching global weights --- .../src/bittensor/subtensor_interface.py | 12 +++-- bittensor_cli/src/commands/subnets.py | 53 ++++++++++++------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index da5fdf92..258097e4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1300,11 +1300,15 @@ async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) return subnets - async def get_global_weight(self, netuid: int, block_hash: Optional[str] = None): - global_weight = await self.substrate.query( + async def get_global_weights( + self, netuids: list[int], block_hash: Optional[str] = None + ): + result = await self.substrate.query_multiple( module="SubtensorModule", storage_function="GlobalWeight", - params=[netuid], + params=[netuid for netuid in netuids], block_hash=block_hash, ) - return u64_normalized_float(global_weight) + return { + netuid: u64_normalized_float(weight) for (netuid, weight) in result.items() + } diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index aa4929a9..743aab19 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -158,25 +158,16 @@ async def subnets_list( ): """List all subnet netuids in the network.""" # TODO add reuse-last and html-output and no-cache - async def fetch_global_weight(netuid): - try: - return netuid, await subtensor.get_global_weight(netuid) - except Exception as e: - print(f"Error fetching global weight for netuid {netuid}: {e}") - return netuid, None - rows = [] subnets = await subtensor.get_all_subnet_dynamic_info() - netuids = [subnet.netuid for subnet in subnets] - - global_weight_tasks = [fetch_global_weight(netuid) for netuid in netuids] - global_weights = await asyncio.gather(*global_weight_tasks) + global_weights = await subtensor.get_global_weights( + [subnet.netuid for subnet in subnets] + ) - global_weight_dict = {netuid: weight for netuid, weight in global_weights} for subnet in subnets: netuid = subnet.netuid - global_weight = global_weight_dict.get(netuid) + global_weight = global_weights.get(netuid) symbol = f"{subnet.symbol}\u200e" if netuid == 0: @@ -196,7 +187,9 @@ async def fetch_global_weight(netuid): f"{global_weight:.4f}" if global_weight is not None else "N/A", ) ) - total_emissions = sum(float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0) + total_emissions = sum( + float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 + ) table = Table( title=f"[underline dark_orange]Subnets[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", @@ -212,11 +205,33 @@ async def fetch_global_weight(netuid): table.add_column("[bold white]NETUID", style="white", justify="center") table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") - table.add_column(f"[bold white]EMISSION ({Balance.get_unit(0)})", style="light_goldenrod2", justify="right", footer=f"τ {total_emissions:.4f}") - table.add_column(f"[bold white]TAO ({Balance.get_unit(0)})", style="rgb(42,161,152)", justify="right") - table.add_column(f"[bold white]STAKE ({Balance.get_unit(1)})", style="light_salmon3", justify="right") - table.add_column(f"[bold white]RATE ({Balance.get_unit(1)}/{Balance.get_unit(0)})", style="medium_purple", justify="right") - table.add_column("[bold white]Tempo (k/n)", style="bright_magenta", justify="right", overflow="fold") + table.add_column( + f"[bold white]EMISSION ({Balance.get_unit(0)})", + style="light_goldenrod2", + justify="right", + footer=f"τ {total_emissions:.4f}", + ) + table.add_column( + f"[bold white]TAO ({Balance.get_unit(0)})", + style="rgb(42,161,152)", + justify="right", + ) + table.add_column( + f"[bold white]STAKE ({Balance.get_unit(1)})", + style="light_salmon3", + justify="right", + ) + table.add_column( + f"[bold white]RATE ({Balance.get_unit(1)}/{Balance.get_unit(0)})", + style="medium_purple", + justify="right", + ) + table.add_column( + "[bold white]Tempo (k/n)", + style="bright_magenta", + justify="right", + overflow="fold", + ) table.add_column("[bold white]Global weight (γ)", style="green", justify="right") # Sort rows by subnet.emission.tao, keeping the first subnet in the first position From 47d350f2097f9a402f39f6c1caac42e0cae565b9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 18:13:45 -0700 Subject: [PATCH 075/332] Improves subnets show --- bittensor_cli/src/commands/subnets.py | 183 ++++++-------------------- 1 file changed, 42 insertions(+), 141 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 743aab19..a27d5c36 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -280,54 +280,44 @@ async def show_root(): ) return - console_width = console.width - 5 table = Table( - title="[white]Root Network", - # width=console_width, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - # expand=True, - show_header=True, + title=f"[underline dark_orange]Root Network[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, + show_lines=False, + pad_edge=True, ) - # Add columns to the table + table.add_column("[bold white]Position", style="white", justify="center") table.add_column( - "Position", style="rgb(253,246,227)", no_wrap=True, justify="center" + f"[bold white] TAO ({Balance.get_unit(0)})", + style="medium_purple", + justify="center", ) table.add_column( - f"TAO ({Balance.get_unit(0)})", - style="medium_purple", - no_wrap=True, + f"[bold white]Stake ({Balance.get_unit(0)})", + style="rgb(42,161,152)", justify="center", ) table.add_column( - f"Stake ({Balance.get_unit(0)})", - style="dark_sea_green", - no_wrap=True, + f"[bold white]Emission ({Balance.get_unit(0)}/block)", + style="light_goldenrod2", justify="center", ) table.add_column( - f"Emission ({Balance.get_unit(0)}/block)", - style="rgb(42,161,152)", - no_wrap=True, + "[bold white]Hotkey", + style="bright_magenta", justify="center", ) table.add_column( - "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + "[bold white]Coldkey", + style="bright_magenta", + justify="center", ) + sorted_hotkeys = sorted( enumerate(root_state.hotkeys), key=lambda x: root_state.global_stake[x[0]], @@ -349,21 +339,22 @@ async def show_root(): str(root_state.local_stake[idx]), str(total_emission_per_block), f"{root_state.hotkeys[idx]}", + f"{root_state.coldkeys[idx]}", ) # Print the table console.print(table) console.print( """ - Description: - The table displays the root subnet participants and their metrics. - The columns are as follows: - - Position: The sorted position of the hotkey by total TAO. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. - - Stake: The stake balance of this hotkey on root (measured in TAO). - - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - - Hotkey: The hotkey ss58 address. - """ +Description: + The table displays the root subnet participants and their metrics. + The columns are as follows: + - Position: The sorted position of the hotkey by total TAO. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on root (measured in TAO). + - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. + - Hotkey: The hotkey ss58 address. +""" ) async def show_subnet(netuid_: int): @@ -385,101 +376,17 @@ async def show_subnet(netuid_: int): return # Define table properties - console_width = console.width - 5 table = Table( - title=f"[white]Subnet {netuid_} Metagraph", - width=console_width, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"[underline dark_orange]Subnet {netuid_}[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, - ) - subnet_info_table = Table( - width=console_width, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, - show_footer=False, - show_edge=False, show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", - title_justify="center", - highlight=False, - ) - - subnet_info_table.add_column( - "Index", style="grey89", no_wrap=True, justify="center" - ) - subnet_info_table.add_column( - "Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center" - ) - subnet_info_table.add_column( - f"Emission ({Balance.get_unit(0)})", - style="rgb(38,139,210)", - no_wrap=True, - justify="center", - ) - subnet_info_table.add_column( - f"P({Balance.get_unit(0)},", - style="rgb(108,113,196)", - no_wrap=True, - justify="right", - ) - subnet_info_table.add_column( - f"{Balance.get_unit(1)})", - style="rgb(42,161,152)", - no_wrap=True, - justify="left", - ) - subnet_info_table.add_column( - f"{Balance.get_unit(1)}", - style="rgb(133,153,0)", - no_wrap=True, - justify="center", - ) - subnet_info_table.add_column( - f"Rate ({Balance.get_unit(1)}/{Balance.get_unit(0)})", - style="rgb(181,137,0)", - no_wrap=True, - justify="center", - ) - subnet_info_table.add_column( - "Tempo", style="rgb(38,139,210)", no_wrap=True, justify="center" - ) - subnet_info_table.add_row( - str(netuid_), - f"[light_goldenrod1]{str(subnet_info.symbol)}[light_goldenrod1]", - f"τ{subnet_info.emission.tao:.4f}", - f"P( τ{subnet_info.tao_in.tao:,.4f},", - f"{subnet_info.alpha_in.tao:,.4f}{subnet_info.symbol} )", - f"{subnet_info.alpha_out.tao:,.4f}{subnet_info.symbol}", - f"{subnet_info.price.tao:.4f}τ/{subnet_info.symbol}", - str(subnet_info.blocks_since_last_step) + "/" + str(subnet_info.tempo), + pad_edge=True, ) - rows = [] emission_sum = sum( [ @@ -522,7 +429,7 @@ async def show_subnet(netuid_: int): ) table.add_column( f"Stake({Balance.get_unit(netuid_)})", - style="green", + style="rgb(42,161,152)", no_wrap=True, justify="right", footer=f"{stake_sum.set_unit(subnet_info.netuid)}", @@ -533,36 +440,30 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="center", ) - table.add_column( - "Dividends", style="rgb(181,137,0)", no_wrap=True, justify="center" - ) - table.add_column( - "Incentive", style="rgb(220,50,47)", no_wrap=True, justify="center" - ) + table.add_column("Dividends", style="#8787d7", no_wrap=True, justify="center") + table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") table.add_column( f"Emission ({Balance.get_unit(netuid_)})", - style="aquamarine3", + style="light_goldenrod2", no_wrap=True, justify="center", footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), ) table.add_column( - "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + "Hotkey", style="bright_magenta", no_wrap=True, justify="center" ) table.add_column( - "Coldkey", style="bold dark_green", no_wrap=True, justify="center" + "Coldkey", style="bright_magenta", no_wrap=True, justify="center" ) for row in rows: table.add_row(*row) # Print the table - # console.print("\n\n\n") - # console.print(subnet_info_table) console.print("\n\n") console.print(table) console.print("\n") console.print( - f"Subnet: {netuid_}:\n Owner: [light_salmon3]{subnet_info.owner}[/light_salmon3]\n Total Locked: [green]{subnet_info.total_locked}[/green]\n Owner Locked: [green]{subnet_info.owner_locked}[/green]" + f"Subnet: {netuid_}:\n Owner: [bold bright_magenta]{subnet_info.owner}[/bold bright_magenta]\n Total Locked: [green]{subnet_info.total_locked}[/green]\n Owner Locked: [green]{subnet_info.owner_locked}[/green]" ) console.print( """ From 82adb4940320a60618aa63962240a2d03a5804d3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 24 Oct 2024 18:39:47 -0700 Subject: [PATCH 076/332] Default to rao network --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 3ab18fe2..3c26ea8f 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -97,7 +97,7 @@ class config: } class subtensor: - network = "finney" + network = "rao" chain_endpoint = None _mock = False From 4ae70084223fffab2af6ac21d8aa714ac20a9cf7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 14:49:03 +0200 Subject: [PATCH 077/332] Mostly fix child hotkeys get. Still issue with 'Storage function "SubtensorModule.ChildkeyTake" not found' --- bittensor_cli/cli.py | 2 +- .../bittensor/async_substrate_interface.py | 17 +++- .../src/bittensor/extrinsics/registration.py | 3 +- .../src/bittensor/subtensor_interface.py | 42 ++++++++-- .../src/commands/stake/children_hotkeys.py | 83 +++++++------------ bittensor_cli/src/commands/subnets.py | 20 ++--- 6 files changed, 87 insertions(+), 80 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f20e82f4..54220f78 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,6 +2,7 @@ import asyncio import binascii import curses +from contextlib import suppress from functools import partial import os.path import re @@ -832,7 +833,6 @@ async def _run(): raise typer.Exit() except SubstrateRequestException as e: err_console.print(str(e)) - asyncio.create_task(cmd).cancel() raise typer.Exit() if sys.version_info < (3, 10): diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index a6baf541..47fc26ee 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -3,7 +3,7 @@ import random from collections import defaultdict from dataclasses import dataclass -from hashlib import blake2b +from hashlib import blake2b, sha256 from typing import Optional, Any, Union, Callable, Awaitable, cast, Iterable from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 @@ -1603,7 +1603,6 @@ async def _make_rpc_request( result_handler: Optional[ResultHandler] = None, ) -> RequestManager.RequestResults: request_manager = RequestManager(payloads) - subscription_added = False async with self.ws as ws: @@ -1786,13 +1785,23 @@ async def query_multiple( # By allowing for specifying the block hash, users, if they have multiple query types they want # to do, can simply query the block hash first, and then pass multiple query_subtensor calls # into an asyncio.gather, with the specified block hash + if len(params) != len(set(params)): + raise SubstrateRequestException( + "You are attempting to query multiple values, but you have duplicates." + ) + block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) if block_hash: self.last_block_hash = block_hash runtime = await self.init_runtime(block_hash=block_hash) preprocessed: tuple[Preprocessed] = await asyncio.gather( *[ - self._preprocess([x] if not isinstance(x, Iterable) else list(x), block_hash, storage_function, module) + self._preprocess( + [x] if not isinstance(x, Iterable) else list(x), + block_hash, + storage_function, + module, + ) for x in params ] ) @@ -1800,10 +1809,10 @@ async def query_multiple( self.make_payload(item.queryable, item.method, item.params) for item in preprocessed ] + # These will always be the same throughout the preprocessed list, so we just grab the first one value_scale_type = preprocessed[0].value_scale_type storage_item = preprocessed[0].storage_item - responses = await self._make_rpc_request( all_info, value_scale_type, storage_item, runtime ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 86bad3c9..5cfead70 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -493,7 +493,6 @@ async def get_neuron_for_pubkey_and_subnet(): ) if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await subtensor.substrate.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", @@ -503,7 +502,7 @@ async def get_neuron_for_pubkey_and_subnet(): if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() - + if result.startswith("0x"): bytes_result = bytes.fromhex(result[2:]) else: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 09801dbf..76847ead 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -420,27 +420,52 @@ async def get_total_stake_for_coldkey( async def get_total_stake_for_hotkey( self, *ss58_addresses, + netuids: Optional[list[int]] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, Balance]: + ) -> dict[str, dict[int, "Balance"]]: """ Returns the total stake held on a hotkey. :param ss58_addresses: The SS58 address(es) of the hotkey(s) + :param netuids: The netuids to retrieve the stake from. If not specified, will use all subnets. :param block_hash: The hash of the block number to retrieve the stake from. :param reuse_block: Whether to reuse the last-used block hash when retrieving info. - :return: {address: Balance objects} + :return: + { + hotkey_ss58_1: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + hotkey_ss58_2: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + ... + } """ - netuids = await self.get_all_subnet_netuids(block_hash=block_hash) - results: dict[tuple[str, int], int] = await self.substrate.query_multiple( - params=[p for p in zip(ss58_addresses, netuids)], + netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash) + query: dict[tuple[str, int], int] = await self.substrate.query_multiple( + params=[(ss58, netuid) for ss58 in ss58_addresses for netuid in netuids], module="SubtensorModule", storage_function="TotalHotkeyAlpha", block_hash=block_hash, reuse_block_hash=reuse_block, ) - return {k[0]: Balance.from_rao(r or 0) for (k, r) in results.items()} + results: dict[str, dict[int, "Balance"]] = { + hk_ss58: {} for hk_ss58 in ss58_addresses + } + for idx, (_, val) in enumerate(query): + hotkey_ss58 = ss58_addresses[idx // len(netuids)] + netuid = netuids[idx % len(netuids)] + value = (Balance.from_rao(val) if val is not None else Balance(0)).set_unit( + netuid + ) + results[hotkey_ss58][netuid] = value + return results async def current_take( self, @@ -732,7 +757,7 @@ async def neuron_for_uid( """ if uid is None: return NeuronInfo.get_null_neuron() - + hex_bytes_result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neuron", @@ -740,7 +765,6 @@ async def neuron_for_uid( block_hash=block_hash, ) - if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() @@ -1194,7 +1218,7 @@ async def get_subnet_dynamic_info( ) if hex_bytes_result is None: - return None + return None if hex_bytes_result.startswith("0x"): bytes_result = bytes.fromhex(hex_bytes_result[2:]) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 8d260e0a..e250926a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -288,38 +288,6 @@ async def get_children( - If netuid is not specified, generates and prints a summary table of all child hotkeys across all subnets. """ - async def get_total_stake_for_hk(hotkey: str, parent: bool = False): - """ - Fetches and displays the total stake for a specified hotkey from the Subtensor blockchain network. - If `parent` is True, it prints the hotkey and its corresponding stake. - - Parameters: - - hotkey (str): The hotkey for which the stake needs to be fetched. - - parent (bool, optional): A flag to indicate whether the hotkey is the parent key. Defaults to False. - - Returns: - - Balance: The total stake associated with the specified hotkey. - """ - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyStake", - params=[hotkey], - reuse_block_hash=True, - ) - stake = ( - Balance.from_rao(_result.value) - if getattr(_result, "value", None) - else Balance(0) - ) - if parent: - console.print( - f"\nYour Hotkey: [bright_magenta]{hotkey}[/bright_magenta] | Total Stake: [dark_orange]{stake}t[/dark_orange]\n", - end="", - no_wrap=True, - ) - - return stake - async def get_take(child: tuple) -> float: """ Get the take value for a given subtensor, hotkey, and netuid. @@ -367,7 +335,7 @@ async def _render_table( console.print(table) console.print( f"[bold red]There are currently no child hotkeys with parent hotkey: " - f"{wallet.name} ({parent_hotkey}).[/bold red]" + f"{wallet.name} | {wallet.hotkey_str} ({parent_hotkey}).[/bold red]" ) return @@ -376,6 +344,24 @@ async def _render_table( total_stake_weight = 0 netuid_children_.sort(key=lambda x: x[0]) # Sort by netuid in ascending order + unique_keys = set( + [parent_hotkey] + + [s for _, child_list in netuid_children_ for _, s in child_list] + ) + hotkey_stake_dict = await subtensor.get_total_stake_for_hotkey( + *unique_keys, + netuids=[n[0] for n in netuid_children_], + ) + parent_total = sum(hotkey_stake_dict[parent_hotkey].values()) + print("NETUID", netuid) + insert_text = ( + " " + if netuid is None + else f" on netuids: {', '.join(str(n[0]) for n in netuid_children_)} " + ) + console.print( + f"The total stake of parent hotkey '{parent_hotkey}'{insert_text}is {parent_total}." + ) for index, (netuid_, children_) in enumerate(netuid_children_): # calculate totals @@ -383,29 +369,26 @@ async def _render_table( total_stake_weight_per_netuid = 0 avg_take_per_netuid = 0.0 - hotkey_stake_dict = await subtensor.get_total_stake_for_hotkey( - parent_hotkey - ) - hotkey_stake = hotkey_stake_dict.get(parent_hotkey, Balance(0)) + hotkey_stake: dict[int, Balance] = hotkey_stake_dict[parent_hotkey] children_info = [] - child_stakes = await asyncio.gather( - *[get_total_stake_for_hk(c[1]) for c in children_] - ) child_takes = await asyncio.gather(*[get_take(c) for c in children_]) - for child, child_stake, child_take in zip( - children_, child_stakes, child_takes - ): + for child, child_take in zip(children_, child_takes): proportion = child[0] child_hotkey = child[1] # add to totals avg_take_per_netuid += child_take - proportion = u64_to_float(proportion) + converted_proportion = u64_to_float(proportion) children_info.append( - (proportion, child_hotkey, child_stake, child_take) + ( + converted_proportion, + child_hotkey, + hotkey_stake_dict[child_hotkey][netuid_], + child_take, + ) ) children_info.sort( @@ -459,17 +442,16 @@ async def _render_table( if netuid is None: # get all netuids netuids = await subtensor.get_all_subnet_netuids() - await get_total_stake_for_hk(wallet.hotkey.ss58_address, True) netuid_children_tuples = [] - for netuid in netuids: + for netuid_ in netuids: success, children, err_mg = await subtensor.get_children( - wallet.hotkey.ss58_address, netuid + wallet.hotkey.ss58_address, netuid_ ) if children: - netuid_children_tuples.append((netuid, children)) + netuid_children_tuples.append((netuid_, children)) if not success: err_console.print( - f"Failed to get children from subtensor {netuid}: {err_mg}" + f"Failed to get children from subtensor {netuid_}: {err_mg}" ) await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) else: @@ -478,7 +460,6 @@ async def _render_table( ) if not success: err_console.print(f"Failed to get children from subtensor: {err_mg}") - await get_total_stake_for_hk(wallet.hotkey.ss58_address, True) if children: netuid_children_tuples = [(netuid, children)] await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 0e502f1e..d2eea0ce 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -273,13 +273,11 @@ async def show_root(): if (bytes_result := hex_bytes_result) is None: err_console.print("The root subnet does not exist") return - + if bytes_result.startswith("0x"): bytes_result = bytes.fromhex(bytes_result[2:]) - root_state: "SubnetState" = SubnetState.from_vec_u8( - bytes_result - ) + root_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) if len(root_state.hotkeys) == 0: err_console.print( "The root-subnet is currently empty with 0 UIDs registered." @@ -373,13 +371,11 @@ async def show_subnet(netuid_: int): if (bytes_result := hex_bytes_result) is None: err_console.print(f"Subnet {netuid_} does not exist") return - + if bytes_result.startswith("0x"): bytes_result = bytes.fromhex(bytes_result[2:]) - - subnet_state: "SubnetState" = SubnetState.from_vec_u8( - bytes_result - ) + + subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) if subnet_info is None: err_console.print(f"Subnet {netuid_} does not exist") return @@ -715,13 +711,11 @@ async def metagraph_cmd( if not (bytes_result := hex_bytes_result): err_console.print(f"Subnet {netuid} does not exist") return - + if bytes_result.startswith("0x"): bytes_result = bytes.fromhex(bytes_result[2:]) - subnet_state: "SubnetState" = SubnetState.from_vec_u8( - bytes_result - ) + subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) difficulty = int(difficulty_) total_issuance = Balance.from_rao(total_issuance_) From 85fd45133513a690e5c96fab7d1d352a999b5825 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 15:01:51 +0200 Subject: [PATCH 078/332] Removed unused imports --- bittensor_cli/cli.py | 1 - bittensor_cli/src/bittensor/async_substrate_interface.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 8 ++------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 54220f78..dda2d9d7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,7 +2,6 @@ import asyncio import binascii import curses -from contextlib import suppress from functools import partial import os.path import re diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 47fc26ee..ebe0e9f5 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -3,7 +3,7 @@ import random from collections import defaultdict from dataclasses import dataclass -from hashlib import blake2b, sha256 +from hashlib import blake2b from typing import Optional, Any, Union, Callable, Awaitable, cast, Iterable from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 668e85ab..078f6e9c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1,8 +1,4 @@ import asyncio -import copy -import json -import sqlite3 -from contextlib import suppress from functools import partial from typing import TYPE_CHECKING, Optional, Sequence, Union, cast @@ -10,13 +6,13 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, FloatPrompt, Prompt -from rich.table import Table, Column -import typer +from rich.table import Table from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( + # TODO add back in caching console, create_table, err_console, From 3dded35314ad0a1169e928e5d370aa10f36ec2b1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 17:57:19 +0200 Subject: [PATCH 079/332] Child hotkey get fixed. --- .../src/commands/stake/children_hotkeys.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index e250926a..52d30dd1 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -288,7 +288,7 @@ async def get_children( - If netuid is not specified, generates and prints a summary table of all child hotkeys across all subnets. """ - async def get_take(child: tuple) -> float: + async def get_take(child: tuple, netuid__: int) -> float: """ Get the take value for a given subtensor, hotkey, and netuid. @@ -299,7 +299,7 @@ async def get_take(child: tuple) -> float: """ child_hotkey = child[1] take_u16 = await get_childkey_take( - subtensor=subtensor, hotkey=child_hotkey, netuid=netuid + subtensor=subtensor, hotkey=child_hotkey, netuid=netuid__ ) if take_u16: return u16_to_float(take_u16) @@ -353,7 +353,6 @@ async def _render_table( netuids=[n[0] for n in netuid_children_], ) parent_total = sum(hotkey_stake_dict[parent_hotkey].values()) - print("NETUID", netuid) insert_text = ( " " if netuid is None @@ -372,7 +371,9 @@ async def _render_table( hotkey_stake: dict[int, Balance] = hotkey_stake_dict[parent_hotkey] children_info = [] - child_takes = await asyncio.gather(*[get_take(c) for c in children_]) + child_takes = await asyncio.gather( + *[get_take(c, netuid_) for c in children_] + ) for child, child_take in zip(children_, child_takes): proportion = child[0] child_hotkey = child[1] @@ -395,9 +396,11 @@ async def _render_table( key=lambda x: x[0], reverse=True ) # sorting by proportion (highest first) - for proportion, hotkey, stake, child_take in children_info: - proportion_percent = proportion * 100 # Proportion in percent - proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO + for proportion_, hotkey, stake, child_take in children_info: + proportion_percent = proportion_ * 100 # Proportion in percent + proportion_tao = ( + hotkey_stake[netuid_].tao * proportion_ + ) # Proportion in TAO total_proportion_per_netuid += proportion_percent @@ -407,7 +410,7 @@ async def _render_table( total_stake_weight_per_netuid += stake_weight take_str = f"{child_take * 100:.3f}%" - hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") + hotkey = Text(hotkey, style="italic red" if proportion_ == 0 else "") table.add_row( str(netuid_), hotkey, From 1e85564ab5959c6d18d42c98bdca4198c9facaa0 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 25 Oct 2024 08:59:29 -0700 Subject: [PATCH 080/332] ui tweaks --- bittensor_cli/src/commands/stake/stake.py | 35 ++++++++--------------- bittensor_cli/src/commands/subnets.py | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 078f6e9c..76186a37 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1497,7 +1497,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): else 0 ) slippage_percentage = ( - f"[dark_red]{slippage_percentage_:.3f}%[/dark_red]" + f"[salmon1]{slippage_percentage_:.3f}%[/salmon1]" ) else: slippage_percentage = "0.000%" @@ -1543,47 +1543,36 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") table = Table( - title=f"[white]hotkey:[/white] [light_salmon3]{name}[/light_salmon3]\n", - width=console.width - 5, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"\n[dark_orange]Hotkey: {name}[/dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, + show_lines=False, + pad_edge=True, ) table.add_column("[white]Netuid", footer_style="overline white", style="grey89") table.add_column( "[white]Symbol", footer_style="white", style="light_goldenrod1", - justify="right", + justify="center", width=5, no_wrap=True, ) table.add_column( f"[white]TAO({Balance.unit})", - style="aquamarine3", + style="medium_purple", justify="right", footer=f"{total_global_tao}", ) table.add_column( f"[white]Stake({Balance.get_unit(1)})", footer_style="overline white", - style="green", - justify="right", + style="rgb(42,161,152)", + justify="center", ) table.add_column( f"[white]Rate({Balance.unit}/{Balance.get_unit(1)})", @@ -1608,7 +1597,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): table.add_column("[white]Registered", style="red", justify="right") table.add_column( f"[white]Emission({Balance.get_unit(1)}/block)", - style="aquamarine3", + style="light_goldenrod2", justify="right", ) table.add_column( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index d2eea0ce..03807851 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -232,7 +232,7 @@ async def subnets_list( justify="right", overflow="fold", ) - table.add_column("[bold white]Global weight (γ)", style="green", justify="right") + table.add_column("[bold white]Global weight (γ)", style="green", justify="center") # Sort rows by subnet.emission.tao, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) From bfa33ee1fb00973fca2cc871f12ad446a2fc5a6f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 18:08:15 +0200 Subject: [PATCH 081/332] Stake fix --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 76186a37..e2d939d8 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1020,7 +1020,7 @@ async def stake_add( # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", f"{hotkey[1]}", str(amount_to_stake_as_balance), - str(1 / float(dynamic_info.price)) + str(1 / float(dynamic_info.price) or 1) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", str(received_amount.set_unit(netuid)), str(slippage_pct), From 8a40a8d011d581e44cb4faf27c1dbcd39487a585 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 18:10:04 +0200 Subject: [PATCH 082/332] Balance fix --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 76847ead..0fa1fc9d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -405,7 +405,7 @@ async def get_total_stake_for_coldkey( tao_locked = pool.tao_in issuance = pool.alpha_out if pool.is_dynamic else tao_locked - tao_ownership = 0 + tao_ownership = Balance(0) if alpha_value.tao > 0.00009 and issuance.tao != 0: tao_ownership = Balance.from_tao( From d19b9f2b8e7d11f498281e712354980f641e536a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 18:16:56 +0200 Subject: [PATCH 083/332] Parentheses matter --- bittensor_cli/src/commands/stake/stake.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index e2d939d8..489c53c4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1020,7 +1020,7 @@ async def stake_add( # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", f"{hotkey[1]}", str(amount_to_stake_as_balance), - str(1 / float(dynamic_info.price) or 1) + str(1 / (float(dynamic_info.price) or 1)) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", str(received_amount.set_unit(netuid)), str(slippage_pct), @@ -1496,9 +1496,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): if slippage + swapped_tao_value != 0 else 0 ) - slippage_percentage = ( - f"[salmon1]{slippage_percentage_:.3f}%[/salmon1]" - ) + slippage_percentage = f"[salmon1]{slippage_percentage_:.3f}%[/salmon1]" else: slippage_percentage = "0.000%" tao_locked = pool.tao_in From 09721c415a3ff2d47de57dc5fba07f7b140f95b8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 25 Oct 2024 18:25:43 +0200 Subject: [PATCH 084/332] Error-handling for wallet unlock --- bittensor_cli/src/commands/stake/stake.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 489c53c4..53be50ad 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1125,7 +1125,11 @@ async def send_extrinsic( ) # Perform staking operation. - wallet.unlock_coldkey() + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False extrinsics_coroutines = [ send_extrinsic(ni, am, curr, staking_address) for i, (ni, am, curr) in enumerate( @@ -1386,7 +1390,11 @@ async def unstake( ) # Perform staking operation. - wallet.unlock_coldkey() + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False with console.status( f"\n:satellite: Unstaking {amount_to_unstake_as_balance} from {staking_address_name} on netuid: {netuid} ..." ): @@ -1812,7 +1820,11 @@ async def move_stake( return True # Perform staking operation. - wallet.unlock_coldkey() + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False with console.status( f"\n:satellite: Moving {amount_to_move_as_balance} from {origin_hotkey_ss58} on netuid: {origin_netuid} to " f"{destination_hotkey} on netuid: {destination_netuid} ..." From 0c55df876cc7d09e9cc5b72466ad5de881019c50 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 25 Oct 2024 09:29:24 -0700 Subject: [PATCH 085/332] Remove lock in st list --- bittensor_cli/src/commands/stake/stake.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 53be50ad..9db63a39 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1492,7 +1492,6 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): else (f" 1.0000 τ/{symbol} ") ) alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid) - locked_value = Balance.from_rao(int(substake_.locked.rao)).set_unit(netuid) tao_value = pool.alpha_to_tao(alpha_value) total_tao_value += tao_value swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( @@ -1544,7 +1543,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): str(Balance.from_tao(per_block_emission).set_unit(netuid)) if substake_.is_registered else "[dark_red]N/A[/dark_red]", # emission per block. - f"[light_slate_blue]{locked_value}[/light_slate_blue]", # Locked value + # f"[light_slate_blue]{locked_value}[/light_slate_blue]", # Locked value ] ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") @@ -1606,12 +1605,6 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): style="light_goldenrod2", justify="right", ) - table.add_column( - f"[white]Locked({Balance.get_unit(1)})", - footer_style="overline white", - style="green", - justify="right", - ) for row in rows: table.add_row(*row) console.print(table) From 667da15432cad93bff10ad5c042d915b4648cb7e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 25 Oct 2024 09:48:56 -0700 Subject: [PATCH 086/332] Fix emission, dividents disply in st show --- bittensor_cli/src/commands/subnets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 03807851..51295d05 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -341,7 +341,7 @@ async def show_root(): str((pos + 1)), str(root_state.global_stake[idx]), str(root_state.local_stake[idx]), - str(total_emission_per_block), + f"{str(total_emission_per_block):.4f}", f"{root_state.hotkeys[idx]}", f"{root_state.coldkeys[idx]}", ) @@ -421,9 +421,9 @@ async def show_subnet(netuid_: int): f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake f"{subnet_state.stake_weight[idx]:.4f}", # Weight # str(subnet_state.dividends[idx]), - f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", # Dividends + f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Dividents str(subnet_state.incentives[idx]), # Incentive - f"{str(Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao)} {subnet_info.symbol}", # Emission + f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emission f"{subnet_state.hotkeys[idx]}", # Hotkey f"{subnet_state.coldkeys[idx]}", # Coldkey ) From 4b58e89b33c6594e369bfd9b7869b69930065af5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 25 Oct 2024 16:34:16 -0700 Subject: [PATCH 087/332] Revamped btcli stake remove --- bittensor_cli/cli.py | 11 +- bittensor_cli/src/commands/stake/stake.py | 349 +++++++++++++--------- 2 files changed, 214 insertions(+), 146 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dda2d9d7..7ac8d4bc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -291,12 +291,14 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - "[green]Enter the netuid to use. Leave blank for all netuids", + "[green]Enter the netuid to use.[/green] Leave blank for all netuids", default=None, show_default=False, ) if answer is None: return None + if answer.lower() == "all": + return None else: return int(answer) else: @@ -2688,8 +2690,9 @@ def stake_remove( ) raise typer.Exit() - if not unstake_all and not amount and not keep_stake: - amount = FloatPrompt.ask("[blue bold]Amount to unstake (TAO τ)[/blue bold]") + # TODO: We are prompting for amount for each subnet later - confirm this to be removed + # if not unstake_all and not amount and not keep_stake: + # amount = FloatPrompt.ask("[blue bold]Amount to unstake (TAO τ)[/blue bold]") if unstake_all and not amount: if not Confirm.ask("Unstake all staked TAO tokens?", default=False): @@ -2702,7 +2705,7 @@ def stake_remove( and not include_hotkeys ): hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from" + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from. [Press Enter to use config values]" ) if is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 9db63a39..2aee12b6 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1160,6 +1160,7 @@ async def send_extrinsic( await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block +# TODO: Decouple stuff from here. Works but is a mess async def unstake( wallet: Wallet, subtensor: "SubtensorInterface", @@ -1183,29 +1184,28 @@ async def unstake( hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] if hotkey_ss58_address: print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") - # Unstake to specific hotkey. + # Unstake from specific hotkey. hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] elif all_hotkeys: print_verbose("Unstaking from all hotkeys") - # Unstake to all hotkeys. + # Unstake from all hotkeys. all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) # Exclude hotkeys that are specified. hotkeys_to_unstake_from = [ (wallet.hotkey_str, wallet.hotkey.ss58_address) for wallet in all_hotkeys_ if wallet.hotkey_str not in exclude_hotkeys - ] # definitely wallets - + ] elif include_hotkeys: print_verbose("Unstaking from included hotkeys") - # Unstake to specific hotkeys. + # Unstake from specific hotkeys. for hotkey_ss58_or_hotkey_name in include_hotkeys: if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): # If the hotkey is a valid ss58 address, we add it to the list. hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) else: # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. + # We then get the hotkey from the wallet and add it to the list. wallet_ = Wallet( name=wallet.name, path=wallet.path, @@ -1216,58 +1216,41 @@ async def unstake( ) else: # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. + # So we unstake from that single hotkey. print_verbose( f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" ) assert wallet.hotkey is not None - hotkeys_to_unstake_from = [(None, wallet.hotkey.ss58_address)] - - final_hotkeys: list[tuple[str, str]] = [] - final_amounts: list[Union[float, Balance]] = [] - hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) + hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] - # Get old staking balance. - table = Table( - title=f"[white]Unstake operation to Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", - width=console.width - 5, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, - show_footer=True, - show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", - title_justify="center", - highlight=False, - ) - rows = [] - unstake_amount_balance = [] - current_stake_balances = [] + # Prepare lists to store unstaking data per subnet + unstake_operations = [] total_received_amount = Balance.from_tao(0) current_wallet_balance: Balance = ( await subtensor.get_balance(wallet.coldkeypub.ss58_address) )[wallet.coldkeypub.ss58_address] max_float_slippage = 0 - non_zero_netuids = [] - # TODO gather this all + + # Iterate over hotkeys and netuids to collect unstake operations for hotkey in hotkeys_to_unstake_from: staking_address_name, staking_address_ss58 = hotkey + initial_amount = amount + skip_remaining_subnets = False # Flag to check if user wants to quit + + if len(netuids) > 1: + console.print( + "[green]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes." + ) + for netuid in netuids: + if skip_remaining_subnets: + break # Exit the loop over netuids + # Check that the subnet exists. dynamic_info = await subtensor.get_subnet_dynamic_info(netuid) if dynamic_info is None: console.print(f"[red]Subnet: {netuid} does not exist.[/red]") - return False + continue # Skip to the next subnet current_stake_balance: Balance = ( await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( @@ -1277,45 +1260,87 @@ async def unstake( ) ) if current_stake_balance.tao == 0: - continue - non_zero_netuids.append(netuid) - current_stake_balances.append(current_stake_balance) + continue # No stake to unstake - # Determine the amount we are staking. - if amount: - amount_to_unstake_as_balance = Balance.from_tao(amount) + # Determine the amount we are unstaking. + if initial_amount: + amount_to_unstake_as_balance = Balance.from_tao(initial_amount) elif unstake_all: amount_to_unstake_as_balance = current_stake_balance - else: # TODO max_stake - if Confirm.ask( - f"Unstake all: [bold]{current_stake_balance}[/bold]" - f" from [bold]{staking_address_name}[/bold] on netuid: {netuid}?" - ): - amount_to_unstake_as_balance = current_stake_balance - else: - try: - # TODO improve this - amount = float( - Prompt.ask( - f"Enter amount to unstake in {Balance.get_unit(netuid)} from subnet: {netuid}" + else: + # Prompt the user for each subnet + while True: + response = Prompt.ask( + f"Unstake all: [dark_orange]{current_stake_balance}[/dark_orange]" + f" from [bright_magenta]{staking_address_name if staking_address_name else staking_address_ss58}[/bright_magenta] on netuid: [dark_orange]{netuid}? [y/n/q]", + choices=["y", "n", "q"], + default="n", + show_choices=True, + ).lower() + + if response.lower() == "q": + skip_remaining_subnets = True + break # Exit the loop over netuids + + elif response.lower() == "y": + amount_to_unstake_as_balance = current_stake_balance + break # Proceed with unstake operation + elif response.lower() == "n": + while True: + amount_input = Prompt.ask( + f"Enter amount to unstake in [dark_orange]{Balance.get_unit(netuid)}[/dark_orange] from subnet: [dark_orange]{netuid}[/dark_orange] (Max: [dark_orange]{current_stake_balance}[/dark_orange])" ) + if amount_input.lower() == "q": + skip_remaining_subnets = True + break # Exit the loop over netuids + + try: + amount_value = float(amount_input) + if amount_value < 0 or amount_value == 0: + console.print( + "[red]Amount cannot be negative or zero.[/red]" + ) + continue # Re-prompt + + amount_to_unstake_as_balance = Balance.from_tao( + amount_value + ) + amount_to_unstake_as_balance.set_unit(netuid) + if amount_to_unstake_as_balance > current_stake_balance: + console.print( + f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" + ) + continue # Re-prompt + + break # Valid amount entered + + except ValueError: + console.print( + "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" + ) + + if skip_remaining_subnets: + break # Exit the loop over netuids + + break # Exit the response loop + + else: + console.print( + "[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]" ) - amount_to_unstake_as_balance = Balance.from_tao(amount) - except ValueError: - err_console.print( - ":cross_mark:[red]Invalid amount Please use `--amount` with `--no-prompt`.[/red]" - ) - return False - unstake_amount_balance.append(amount_to_unstake_as_balance) + continue # Re-prompt - # Check enough to stake. + if skip_remaining_subnets: + break # Exit the loop over netuids + + # Check enough stake to remove. amount_to_unstake_as_balance.set_unit(netuid) if amount_to_unstake_as_balance > current_stake_balance: err_console.print( - f"[red]Not enough stake to remove[/red]:[bold white]\n stake balance:{current_stake_balance}" - f" < unstaking amount: {amount_to_unstake_as_balance}[/bold white]" + f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]" + f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]" ) - return False + continue # Skip to the next subnet received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( amount_to_unstake_as_balance @@ -1333,21 +1358,40 @@ async def unstake( slippage_pct = f"{slippage_pct_float}%" max_float_slippage = max(max_float_slippage, slippage_pct_float) - rows.append( - ( - str(netuid), - # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", - f"{staking_address_ss58}", - str(amount_to_unstake_as_balance), - str(float(dynamic_info.price)) - + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", - str(received_amount), - str(slippage_pct), - ) + unstake_operations.append( + { + "netuid": netuid, + "hotkey_name": staking_address_name + if staking_address_name + else staking_address_ss58, + "hotkey_ss58": staking_address_ss58, + "amount_to_unstake": amount_to_unstake_as_balance, + "current_stake_balance": current_stake_balance, + "received_amount": received_amount, + "slippage_pct": slippage_pct, + "slippage_pct_float": slippage_pct_float, + "dynamic_info": dynamic_info, + } ) + if not unstake_operations: + console.print("[red]No unstake operations to perform.[/red]") + return False + + # Build the table + table = Table( + title=f"\n[dark_orange]Unstaking to: \nWallet: [light_goldenrod2]{wallet.name}[/light_goldenrod2], Coldkey ss58: [light_goldenrod2]{wallet.coldkeypub.ss58_address}[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style="light_salmon3") + table.add_column("Hotkey", justify="center", style="bright_magenta") table.add_column( f"Amount ({Balance.get_unit(1)})", justify="center", style="dark_sea_green" ) @@ -1357,24 +1401,35 @@ async def unstake( style="light_goldenrod2", ) table.add_column( - f"Recieved ({Balance.get_unit(0)})", + f"Received ({Balance.get_unit(0)})", justify="center", style="light_slate_blue", footer=f"{total_received_amount}", ) table.add_column("Slippage", justify="center", style="rgb(220,50,47)") - for row in rows: - table.add_row(*row) + + for op in unstake_operations: + dynamic_info = op["dynamic_info"] + table.add_row( + str(op["netuid"]), + op["hotkey_name"], + str(op["amount_to_unstake"]), + str(float(dynamic_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})", + str(op["received_amount"]), + op["slippage_pct"], + ) + console.print(table) - message = "" + if max_float_slippage > 5: - message += f"-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold][yellow]WARNING:[/yellow]\tThe slippage on one of your operations is high: [bold red]{max_float_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" - message += f"-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - if prompt: - if not Confirm.ask("Would you like to continue?"): - return False + console.print( + "\n" + f"-------------------------------------------------------------------------------------------------------------------\n" + f"[bold][yellow]WARNING:[/yellow] The slippage on one of your operations is high: [bold red]{max_float_slippage}%[/bold red], this may result in a loss of funds.[/bold] \n" + f"-------------------------------------------------------------------------------------------------------------------\n" + ) + console.print( """ [bold white]Description[/bold white]: @@ -1388,63 +1443,73 @@ async def unstake( - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). """ ) + if prompt: + if not Confirm.ask("Would you like to continue?"): + return False - # Perform staking operation. + # Perform unstaking operations try: wallet.unlock_coldkey() except KeyFileError: err_console.print("Error decrypting coldkey (possibly incorrect password)") return False - with console.status( - f"\n:satellite: Unstaking {amount_to_unstake_as_balance} from {staking_address_name} on netuid: {netuid} ..." - ): - for hotkey in hotkeys_to_unstake_from: - staking_address_name, staking_address_ss58 = hotkey - for netuid_i, amount, current in list( - zip(non_zero_netuids, unstake_amount_balance, current_stake_balances) - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_unstaked": amount.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") + + with console.status(f"\n:satellite: Performing unstaking operations...") as status: + for op in unstake_operations: + netuid_i = op["netuid"] + staking_address_name = op["hotkey_name"] + staking_address_ss58 = op["hotkey_ss58"] + amount = op["amount_to_unstake"] + current_stake_balance = op["current_stake_balance"] + + status.update( + f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..." + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + else: + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) else: - await response.process_events() - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(response.error_message, subtensor.substrate)}" - ) - else: - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=netuid_i, - ) - ).set_unit(netuid_i) - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - console.print( - f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid_i, ) + ).set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + console.print( + f"Subnet: {netuid_i} Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + console.print("[green]Unstaking operations completed.[/green]") async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): From fd5f801e7ceff914ec3e1a899f824dfc5cc0839d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 12:03:28 -0700 Subject: [PATCH 088/332] Updates localnet entry point --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index e36d76b5..9e46757b 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -10,7 +10,7 @@ class Constants: archive_entrypoint = "wss://archive.chain.opentensor.ai:443" rao_entrypoint = "wss://rao.chain.opentensor.ai:443/" dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " - local_entrypoint = "ws://127.0.0.1:9444" + local_entrypoint = "ws://127.0.0.1:9944" network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, From 642103d1c554967efc56e4193a19924cda783dfd Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 12:09:16 -0700 Subject: [PATCH 089/332] Updated stake slippage table --- bittensor_cli/src/commands/stake/stake.py | 28 +++++++---------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2aee12b6..04a8b3cf 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -862,27 +862,15 @@ async def stake_add( ) # Init the table. table = Table( - title="[white]Staking operation from Coldkey SS58[/white]: " - f"[bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n", - width=console.width - 5, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"\n[dark_orange]Staking to: \nWallet: [light_goldenrod2]{wallet.name}[/light_goldenrod2], Coldkey ss58: [light_goldenrod2]{wallet.coldkeypub.ss58_address}[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, + show_lines=False, + pad_edge=True, ) # Determine the amount we are staking. @@ -1027,7 +1015,7 @@ async def stake_add( ) ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style="light_salmon3") + table.add_column("Hotkey", justify="center", style="bright_magenta") table.add_column( f"Amount ({Balance.get_unit(0)})", justify="center", style="dark_sea_green" ) @@ -1048,7 +1036,7 @@ async def stake_add( message = "" if max_slippage > 5: message += "-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold][yellow]WARNING:[/yellow]\tThe slippage on one of your operations is high: [bold red]{max_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" + message += f"[bold][yellow]WARNING:[/yellow] The slippage on one of your operations is high: [bold red]{max_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" message += "-------------------------------------------------------------------------------------------------------------------\n" console.print(message) console.print( From 35b5aee47a724126743d6cf93c46416c5b0342ad Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 12:46:16 -0700 Subject: [PATCH 090/332] Updates s create table + ui tweaks in stake/unstake --- bittensor_cli/src/commands/stake/stake.py | 4 +-- bittensor_cli/src/commands/subnets.py | 33 +++++++---------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 04a8b3cf..4480fb7f 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1109,7 +1109,7 @@ async def send_extrinsic( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) console.print( - f"Subnet: {netuid_i} Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" + f"Subnet: [dark_orange]{netuid_i}[/dark_orange] Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" ) # Perform staking operation. @@ -1495,7 +1495,7 @@ async def unstake( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) console.print( - f"Subnet: {netuid_i} Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [green]{new_stake}[/green]" + f"Subnet: [dark_orange]{netuid_i}[/dark_orange] Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [green]{new_stake}[/green]" ) console.print("[green]Unstaking operations completed.[/green]") diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 51295d05..ff067136 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -597,30 +597,17 @@ async def register( if prompt: # TODO make this a reusable function, also used in subnets list # Show creation table. - console_width = console.width - 5 table = Table( - title="Subnet Info", - width=console_width, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"\n[white]Register to netuid [dark_orange]{netuid}[/dark_orange]\nNetwork: [dark_orange]{subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, + show_lines=False, + pad_edge=True, ) - table.title = f"[white]Register - {subtensor.network}\n" table.add_column( "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" ) @@ -629,15 +616,15 @@ async def register( ) table.add_column( f"Cost ({Balance.get_unit(0)})", - style="rgb(38,139,210)", + style="light_goldenrod2", no_wrap=True, - justify="right", + justify="center", ) table.add_column( - "Hotkey", style="light_salmon3", no_wrap=True, justify="center" + "Hotkey", style="bright_magenta", no_wrap=True, justify="center" ) table.add_column( - "Coldkey", style="bold dark_green", no_wrap=True, justify="center" + "Coldkey", style="bold bright_magenta", no_wrap=True, justify="center" ) table.add_row( str(netuid), From 757cc7fde1fa7b8f8680e66541d0d98484d44c60 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 28 Oct 2024 22:59:52 +0200 Subject: [PATCH 091/332] Fixed NeuronInfoLite decoding. --- bittensor_cli/src/commands/wallets.py | 40 ++++----------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 1b0fe474..f3382a53 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1139,25 +1139,6 @@ async def _fetch_all_neurons( ) -def _partial_decode(args): - """ - Helper function for passing to ProcessPoolExecutor that decodes scale bytes based on a set return type and - rpc type registry, passing this back to the Executor with its specified netuid for easier mapping - - :param args: (return type, scale bytes object, custom rpc type registry, netuid) - - :return: (original netuid, decoded object) - """ - return_type, as_scale_bytes, custom_rpc_type_registry_, netuid_ = args - decoded = decode_scale_bytes(return_type, as_scale_bytes, custom_rpc_type_registry_) - if decoded.startswith("0x"): - bytes_result = bytes.fromhex(decoded[2:]) - else: - bytes_result = bytes.fromhex(decoded) - - return netuid_, NeuronInfoLite.list_from_vec_u8(bytes_result) - - def _process_neurons_for_netuids( netuids_with_all_neurons_hex_bytes: list[tuple[int, list[ScaleBytes]]], ) -> list[tuple[int, list[NeuronInfoLite]]]: @@ -1167,22 +1148,10 @@ def _process_neurons_for_netuids( :param netuids_with_all_neurons_hex_bytes: netuids with hex-bytes neurons :return: netuids mapped to decoded neurons """ - - def make_map(res_): - netuid_, json_result = res_ - hex_bytes_result = json_result["result"] - as_scale_bytes = scalecodec.ScaleBytes(hex_bytes_result) - return [return_type, as_scale_bytes, custom_rpc_type_registry, netuid_] - - return_type = TYPE_REGISTRY["runtime_api"]["NeuronInfoRuntimeApi"]["methods"][ - "get_neurons_lite" - ]["type"] - - preprocessed = [make_map(r) for r in netuids_with_all_neurons_hex_bytes] - with ProcessPoolExecutor() as executor: - results = list(executor.map(_partial_decode, preprocessed)) - - all_results = [(netuid, result) for netuid, result in results] + all_results = [ + (netuid, NeuronInfoLite.list_from_vec_u8(bytes.fromhex(result[2:]))) + for netuid, result in netuids_with_all_neurons_hex_bytes + ] return all_results @@ -1190,6 +1159,7 @@ async def _get_neurons_for_netuids( subtensor: SubtensorInterface, netuids: list[int], hot_wallets: list[str] ) -> list[tuple[int, list["NeuronInfoLite"], Optional[str]]]: all_neurons_hex_bytes = await _fetch_all_neurons(netuids, subtensor) + print(all_neurons_hex_bytes) all_processed_neurons = _process_neurons_for_netuids(all_neurons_hex_bytes) return [ From 13b54077bff5d9f48d9ebb37e5d10da7c891e9b5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 14:04:49 -0700 Subject: [PATCH 092/332] Removes print statement --- bittensor_cli/src/commands/wallets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index f3382a53..bc3b4e69 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1159,7 +1159,6 @@ async def _get_neurons_for_netuids( subtensor: SubtensorInterface, netuids: list[int], hot_wallets: list[str] ) -> list[tuple[int, list["NeuronInfoLite"], Optional[str]]]: all_neurons_hex_bytes = await _fetch_all_neurons(netuids, subtensor) - print(all_neurons_hex_bytes) all_processed_neurons = _process_neurons_for_netuids(all_neurons_hex_bytes) return [ From e61c094b04e5163c2c1336175fe067df279397bf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 14:12:43 -0700 Subject: [PATCH 093/332] Fixed stake move + ui --- bittensor_cli/src/commands/stake/stake.py | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 4480fb7f..c5687301 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1702,7 +1702,6 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): - [bold white]Swap[/bold white]: The amount of TAO received when unstaking all of the hotkey's stake (with slippage). - [bold white]Registered[/bold white]: Whether the hotkey is registered on this subnet. - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. - - [bold white]Locked[/bold white]: The total amount of stake locked (not able to be unstaked). """ ) @@ -1781,7 +1780,7 @@ async def move_stake( subtensor.get_subnet_dynamic_info(origin_netuid), subtensor.get_subnet_dynamic_info(destination_netuid), ) - price = float(dynamic_origin.price) * 1 / float(dynamic_destination.price) + price = float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) received_amount_tao, slippage = dynamic_origin.alpha_to_tao_with_slippage( amount_to_move_as_balance ) @@ -1822,26 +1821,37 @@ async def move_stake( title_justify="center", highlight=False, ) - table.add_column("origin netuid", justify="center", style="rgb(133,153,0)") - table.add_column("origin hotkey", justify="center", style="rgb(38,139,210)") - table.add_column("dest netuid", justify="center", style="rgb(133,153,0)") - table.add_column("dest hotkey", justify="center", style="rgb(38,139,210)") + table = Table( + title=f"\n[dark_orange]Moving stake from: [light_goldenrod2]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/light_goldenrod2] to: [light_goldenrod2]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("origin netuid", justify="center", style="green") + table.add_column("origin hotkey", justify="center", style="bright_magenta") + table.add_column("dest netuid", justify="center", style="green") + table.add_column("dest hotkey", justify="center", style="bright_magenta") table.add_column( f"amount ({Balance.get_unit(origin_netuid)})", justify="center", - style="rgb(38,139,210)", + style="medium_purple", ) table.add_column( f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", justify="center", - style="rgb(42,161,152)", + style="cyan", ) table.add_column( f"received ({Balance.get_unit(destination_netuid)})", justify="center", style="rgb(220,50,47)", ) - table.add_column("slippage", justify="center", style="rgb(181,137,0)") + table.add_column("slippage", justify="center", style="salmon1") table.add_row( f"{Balance.get_unit(origin_netuid)}({origin_netuid})", @@ -1883,7 +1893,7 @@ async def move_stake( "origin_netuid": origin_netuid, "destination_hotkey": destination_hotkey, "destination_netuid": destination_netuid, - "amount_moved": amount_to_move_as_balance.rao, + "alpha_amount": amount_to_move_as_balance.rao, }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( From 3d7d46ddf46254830a593220259fd897ecc8795c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 14:15:20 -0700 Subject: [PATCH 094/332] Removes extra table initialization --- bittensor_cli/src/commands/stake/stake.py | 40 +++++++---------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index c5687301..7c76ab8e 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1780,7 +1780,11 @@ async def move_stake( subtensor.get_subnet_dynamic_info(origin_netuid), subtensor.get_subnet_dynamic_info(destination_netuid), ) - price = float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) + price = ( + float(dynamic_origin.price) + * 1 + / (float(dynamic_destination.price) or 1) + ) received_amount_tao, slippage = dynamic_origin.alpha_to_tao_with_slippage( amount_to_move_as_balance ) @@ -1800,38 +1804,16 @@ async def move_stake( ) table = Table( - title="[white]Move Stake", - width=console.width - 5, - safe_box=True, - padding=(0, 1), - collapse_padding=False, - pad_edge=True, - expand=True, - show_header=True, + title=f"\n[dark_orange]Moving stake from: [light_goldenrod2]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/light_goldenrod2] to: [light_goldenrod2]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", show_footer=True, show_edge=False, - show_lines=False, - leading=0, - style="none", - row_styles=None, - header_style="bold", - footer_style="bold", - border_style="rgb(7,54,66)", - title_style="bold magenta", + header_style="bold white", + border_style="bright_black", + style="bold", title_justify="center", - highlight=False, + show_lines=False, + pad_edge=True, ) - table = Table( - title=f"\n[dark_orange]Moving stake from: [light_goldenrod2]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/light_goldenrod2] to: [light_goldenrod2]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) table.add_column("origin netuid", justify="center", style="green") table.add_column("origin hotkey", justify="center", style="bright_magenta") table.add_column("dest netuid", justify="center", style="green") From 73ddea0bcc9c6384f45af1dce0a83e43717b8d63 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 28 Oct 2024 23:17:32 +0200 Subject: [PATCH 095/332] Registration fix. --- bittensor_cli/src/bittensor/extrinsics/registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 5cfead70..b45b3a5a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -494,7 +494,7 @@ async def get_neuron_for_pubkey_and_subnet(): if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await subtensor.substrate.query_runtime_api( + hex_bytes_result = await subtensor.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neuron", params=[netuid, uid], From f8b302940fa85581b646ca0775efffcd48b02b3e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 28 Oct 2024 15:08:59 -0700 Subject: [PATCH 096/332] btcli stake move improved --- bittensor_cli/cli.py | 4 ++-- bittensor_cli/src/commands/stake/stake.py | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7ac8d4bc..efdc643b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2784,10 +2784,10 @@ def stake_move( None, help="Destination hotkey", prompt=False ), amount: float = typer.Option( - 0.0, + None, "--amount", help="The amount of TAO to stake", - prompt=True, + prompt=False, ), stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 7c76ab8e..ae4076b6 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2,6 +2,7 @@ from functools import partial from typing import TYPE_CHECKING, Optional, Sequence, Union, cast +import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError @@ -1734,6 +1735,19 @@ async def move_stake( ) ).set_unit(destination_netuid) + if origin_stake_balance == Balance.from_tao(0).set_unit(origin_netuid): + print_error( + f"Your balance is [dark_orange]0[/dark_orange] in Netuid: [dark_orange]{origin_netuid}[/dark_orange]" + ) + raise typer.Exit() + + console.print( + f"\nOrigin netuid: [dark_orange]{origin_netuid}[/dark_orange], Origin stake: [dark_orange]{origin_stake_balance}" + ) + console.print( + f"Destination netuid: [dark_orange]{destination_netuid}[/dark_orange], Destination stake: [dark_orange]{destination_stake_balance}\n" + ) + # Determine the amount we are moving. amount_to_move_as_balance = None if amount: @@ -1742,25 +1756,25 @@ async def move_stake( amount_to_move_as_balance = origin_stake_balance else: # max_stake # TODO improve this - if Confirm.ask(f"Move all: [bold]{origin_stake_balance}[/bold]?"): + if Confirm.ask(f"Move all: [dark_orange]{origin_stake_balance}[/dark_orange]?"): amount_to_move_as_balance = origin_stake_balance else: try: amount = float( Prompt.ask( - f"Enter amount to move in {Balance.get_unit(origin_netuid)}" + f"Enter amount to move in [dark_orange]{Balance.get_unit(origin_netuid)}" ) ) amount_to_move_as_balance = Balance.from_tao(amount) except ValueError: - err_console.print(f":cross_mark:[red]Invalid amount: {amount}[/red]") + print_error(f":cross_mark: Invalid amount: {amount}") return False # Check enough to move. amount_to_move_as_balance.set_unit(origin_netuid) if amount_to_move_as_balance > origin_stake_balance: err_console.print( - f"[red]Not enough stake[/red]:[bold white]\n stake balance:{origin_stake_balance} < moving amount: {amount_to_move_as_balance}[/bold white]" + f"[red]Not enough stake[/red]:\n Stake balance:[dark_orange]{origin_stake_balance}[/dark_orange] < Moving amount: [dark_orange]{amount_to_move_as_balance}[/dark_orange]" ) return False From af134ea52c44523b214b9e2cd51d6cb5061fe409 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Oct 2024 13:47:20 +0200 Subject: [PATCH 097/332] Fixed bug where format_error_message was not receiving a substrate object in transfer --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 620c20d7..9d0d5a72 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -98,7 +98,7 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - return False, "", format_error_message(await response.error_message) + return False, "", format_error_message(await response.error_message, subtensor.substrate) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): From fb59bc00bf53bf65752c50f1f80cdaae67fe7de1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 09:04:23 -0700 Subject: [PATCH 098/332] Hides overview, inspect, metagraph --- bittensor_cli/cli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index efdc643b..471197fd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -622,15 +622,15 @@ def __init__(self): self.wallet_app.command( "history", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] )(self.wallet_history) - self.wallet_app.command( - "overview", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] - )(self.wallet_overview) + # self.wallet_app.command( + # "overview", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] + # )(self.wallet_overview) self.wallet_app.command( "transfer", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_transfer) - self.wallet_app.command( - "inspect", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] - )(self.wallet_inspect) + # self.wallet_app.command( + # "inspect", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] + # )(self.wallet_inspect) self.wallet_app.command( "faucet", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_faucet) @@ -717,9 +717,9 @@ def __init__(self): self.subnets_app.command( "register", rich_help_panel=HELP_PANELS["SUBNETS"]["REGISTER"] )(self.subnets_register) - self.subnets_app.command( - "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] - )(self.subnets_metagraph) + # self.subnets_app.command( + # "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + # )(self.subnets_metagraph) self.subnets_app.command( "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) From de3c03d64a08a632f92ec530b5db4dbc0da58afb Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 09:18:18 -0700 Subject: [PATCH 099/332] Fixes set-identity --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 471197fd..d53d2c11 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2319,8 +2319,8 @@ def pgp_check(s: str): twitter_url, info_, validator_id, - prompt, subnet_netuid, + prompt, ) ) From fcd5e1ae10eab3339d6169cd1b42bfb91efba9f7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 09:40:56 -0700 Subject: [PATCH 100/332] Uses coldkeypub in set-identity --- bittensor_cli/src/commands/wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index bc3b4e69..0efe8dc1 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1524,7 +1524,7 @@ async def set_id( return False identified = ( - wallet.hotkey.ss58_address if validator_id else wallet.coldkey.ss58_address + wallet.hotkey.ss58_address if validator_id else wallet.coldkeypub.ss58_address ) encoded_id_dict = { "info": { From 999090998f3b8b2d6f59d8fdf0656fa26a9c6850 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 10:50:07 -0700 Subject: [PATCH 101/332] updates subnets show --- bittensor_cli/src/commands/stake/stake.py | 1 - bittensor_cli/src/commands/subnets.py | 25 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index ae4076b6..86d1aaac 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1618,7 +1618,6 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): footer_style="white", style="light_goldenrod1", justify="center", - width=5, no_wrap=True, ) table.add_column( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index ff067136..28d2862a 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -257,6 +257,7 @@ async def subnets_list( - [bold white]Stake[/bold white]: The outstanding supply of stake across all staking accounts on this subnet. - [bold white]Rate[/bold white]: The rate of conversion between TAO and the subnet's staking unit. - [bold white]Tempo[/bold white]: The number of blocks between epochs. Represented as (k/n) where k is the blocks since the last epoch and n is the total blocks in the epoch. + - [bold white]Global weight[/bold white]: The global weight of the subnet across all subnets. """ ) @@ -341,7 +342,7 @@ async def show_root(): str((pos + 1)), str(root_state.global_stake[idx]), str(root_state.local_stake[idx]), - f"{str(total_emission_per_block):.4f}", + f"{(total_emission_per_block)}", f"{root_state.hotkeys[idx]}", f"{root_state.coldkeys[idx]}", ) @@ -358,6 +359,7 @@ async def show_root(): - Stake: The stake balance of this hotkey on root (measured in TAO). - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. """ ) @@ -406,12 +408,14 @@ async def show_subnet(netuid_: int): ) tao_sum = Balance(0) stake_sum = Balance(0) + relative_emissions_sum = 0 for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum if emission_sum != 0 else 0 ) + relative_emissions_sum += hotkey_block_emission tao_sum += subnet_state.global_stake[idx] stake_sum += subnet_state.local_stake[idx] rows.append( @@ -421,9 +425,10 @@ async def show_subnet(netuid_: int): f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake f"{subnet_state.stake_weight[idx]:.4f}", # Weight # str(subnet_state.dividends[idx]), - f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Dividents + f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends str(subnet_state.incentives[idx]), # Incentive - f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emission + # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative + f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions f"{subnet_state.hotkeys[idx]}", # Hotkey f"{subnet_state.coldkeys[idx]}", # Coldkey ) @@ -450,10 +455,19 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="center", ) - table.add_column("Dividends", style="#8787d7", no_wrap=True, justify="center") + table.add_column("Dividends", style="#8787d7", no_wrap=True, justify="center", footer=f"{relative_emissions_sum:.3f}",) table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") + + # Hiding relative emissions for now + # table.add_column( + # "Emissions", + # style="light_goldenrod2", + # no_wrap=True, + # justify="center", + # footer=f"{relative_emissions_sum:.3f}", + # ) table.add_column( - f"Emission ({Balance.get_unit(netuid_)})", + f"Emissions ({Balance.get_unit(netuid_)})", style="light_goldenrod2", no_wrap=True, justify="center", @@ -488,6 +502,7 @@ async def show_subnet(netuid_: int): - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) - Emission: The emission accrued to this hokey on this subnet every block (in staking units). - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. """ ) From 4b448f092fa5c2dd184366ab698b2c265fb9df45 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Tue, 29 Oct 2024 16:22:15 -0400 Subject: [PATCH 102/332] should be / emission_drain_tempo --- bittensor_cli/src/commands/stake/stake.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 86d1aaac..0e7ee3c5 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1562,9 +1562,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): slippage_percentage = "0.000%" tao_locked = pool.tao_in issuance = pool.alpha_out if pool.is_dynamic else tao_locked - per_block_emission = substake_.emission.tao / ( - (emission_drain_tempo / pool.tempo) * pool.tempo - ) + per_block_emission = substake_.emission.tao / emission_drain_tempo if alpha_value.tao > 0.00009: if issuance.tao != 0: alpha_ownership = "{:.4f}".format( From 9733a957795178d297cd394d35c03d0f348086b5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 13:54:10 -0700 Subject: [PATCH 103/332] Updates color schemes --- bittensor_cli/cli.py | 6 +++--- bittensor_cli/src/commands/stake/stake.py | 24 ++++++++++----------- bittensor_cli/src/commands/subnets.py | 26 +++++++++++------------ bittensor_cli/src/commands/sudo.py | 6 +++--- bittensor_cli/src/commands/wallets.py | 8 +++---- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d53d2c11..159619a4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -291,7 +291,7 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - "[green]Enter the netuid to use.[/green] Leave blank for all netuids", + "[dark_sea_green3]Enter the netuid to use.[/dark_sea_green3] Leave blank for all netuids", default=None, show_default=False, ) @@ -2580,11 +2580,11 @@ def stake_add( raise typer.Exit() if netuid is not None: amount = FloatPrompt.ask( - "[dark_orange]Amount to stake (TAO τ)[/dark_orange]" + "[dark_sea_green]Amount to stake (TAO τ)[/dark_sea_green]" ) else: amount = FloatPrompt.ask( - "[dark_orange]Amount to stake to each netuid (TAO τ)[/dark_orange]" + "[dark_sea_green]Amount to stake to each netuid (TAO τ)[/dark_sea_green]" ) if Balance.from_tao(amount) > free_balance: print_error( diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 0e7ee3c5..8f442158 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1228,7 +1228,7 @@ async def unstake( if len(netuids) > 1: console.print( - "[green]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes." + "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" ) for netuid in netuids: @@ -1260,8 +1260,8 @@ async def unstake( # Prompt the user for each subnet while True: response = Prompt.ask( - f"Unstake all: [dark_orange]{current_stake_balance}[/dark_orange]" - f" from [bright_magenta]{staking_address_name if staking_address_name else staking_address_ss58}[/bright_magenta] on netuid: [dark_orange]{netuid}? [y/n/q]", + f"Unstake all: [dark_sea_green3]{current_stake_balance}[/dark_sea_green3]" + f" from [dark_sea_green3]{staking_address_name if staking_address_name else staking_address_ss58}[/dark_sea_green3] on netuid: [dark_sea_green3]{netuid}? [y/n/q]", choices=["y", "n", "q"], default="n", show_choices=True, @@ -1277,7 +1277,7 @@ async def unstake( elif response.lower() == "n": while True: amount_input = Prompt.ask( - f"Enter amount to unstake in [dark_orange]{Balance.get_unit(netuid)}[/dark_orange] from subnet: [dark_orange]{netuid}[/dark_orange] (Max: [dark_orange]{current_stake_balance}[/dark_orange])" + f"Enter amount to unstake in [dark_sea_green3]{Balance.get_unit(netuid)}[/dark_sea_green3] from subnet: [dark_sea_green3]{netuid}[/dark_sea_green3] (Max: [dark_sea_green3]{current_stake_balance}[/dark_sea_green3])" ) if amount_input.lower() == "q": skip_remaining_subnets = True @@ -1369,7 +1369,7 @@ async def unstake( # Build the table table = Table( - title=f"\n[dark_orange]Unstaking to: \nWallet: [light_goldenrod2]{wallet.name}[/light_goldenrod2], Coldkey ss58: [light_goldenrod2]{wallet.coldkeypub.ss58_address}[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", + title=f"\n[navajo_white1]Unstaking to: \nWallet: [dark_sea_green3]{wallet.name}[/dark_sea_green3], Coldkey ss58: [dark_sea_green3]{wallet.coldkeypub.ss58_address}[/dark_sea_green3]\nNetwork: {subtensor.network}[/navajo_white1]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1380,7 +1380,7 @@ async def unstake( pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style="bright_magenta") + table.add_column("Hotkey", justify="center", style="plum2") table.add_column( f"Amount ({Balance.get_unit(1)})", justify="center", style="dark_sea_green" ) @@ -1395,7 +1395,7 @@ async def unstake( style="light_slate_blue", footer=f"{total_received_amount}", ) - table.add_column("Slippage", justify="center", style="rgb(220,50,47)") + table.add_column("Slippage", justify="center", style="light_salmon3") for op in unstake_operations: dynamic_info = op["dynamic_info"] @@ -1559,7 +1559,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ) slippage_percentage = f"[salmon1]{slippage_percentage_:.3f}%[/salmon1]" else: - slippage_percentage = "0.000%" + slippage_percentage = "[salmon1]0.000%[/salmon1]" tao_locked = pool.tao_in issuance = pool.alpha_out if pool.is_dynamic else tao_locked per_block_emission = substake_.emission.tao / emission_drain_tempo @@ -1680,10 +1680,10 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): console.print("\n\n") console.print( f"Wallet:\n" - f" Coldkey SS58: [bold dark_green]{wallet.coldkeypub.ss58_address}[/bold dark_green]\n" - f" Free Balance: [aquamarine3]{balance}[/aquamarine3]\n" - f" Total TAO ({Balance.unit}): [aquamarine3]{all_hotkeys_total_global_tao}[/aquamarine3]\n" - f" Total Value ({Balance.unit}): [aquamarine3]{all_hotkeys_total_tao_value}[/aquamarine3]" + f" Coldkey SS58: [bold plum2]{wallet.coldkeypub.ss58_address}[/bold plum2]\n" + f" Free Balance: [dark_sea_green]{balance}[/dark_sea_green]\n" + f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" + f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" ) console.print( """ diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 28d2862a..e311b8fd 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -192,7 +192,7 @@ async def subnets_list( ) table = Table( - title=f"[underline dark_orange]Subnets[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", + title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -207,7 +207,7 @@ async def subnets_list( table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") table.add_column( f"[bold white]EMISSION ({Balance.get_unit(0)})", - style="light_goldenrod2", + style="tan", justify="right", footer=f"τ {total_emissions:.4f}", ) @@ -228,11 +228,11 @@ async def subnets_list( ) table.add_column( "[bold white]Tempo (k/n)", - style="bright_magenta", + style="plum2", justify="right", overflow="fold", ) - table.add_column("[bold white]Global weight (γ)", style="green", justify="center") + table.add_column("[bold white]Global weight (γ)", style="dark_sea_green3", justify="center") # Sort rows by subnet.emission.tao, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) @@ -309,17 +309,17 @@ async def show_root(): ) table.add_column( f"[bold white]Emission ({Balance.get_unit(0)}/block)", - style="light_goldenrod2", + style="tan", justify="center", ) table.add_column( "[bold white]Hotkey", - style="bright_magenta", + style="plum2", justify="center", ) table.add_column( "[bold white]Coldkey", - style="bright_magenta", + style="plum2", justify="center", ) @@ -389,7 +389,7 @@ async def show_subnet(netuid_: int): # Define table properties table = Table( - title=f"[underline dark_orange]Subnet {netuid_}[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", + title=f"[underline navajo_white1]Subnet {netuid_}[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -468,16 +468,16 @@ async def show_subnet(netuid_: int): # ) table.add_column( f"Emissions ({Balance.get_unit(netuid_)})", - style="light_goldenrod2", + style="tan", no_wrap=True, justify="center", footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), ) table.add_column( - "Hotkey", style="bright_magenta", no_wrap=True, justify="center" + "Hotkey", style="plum2", no_wrap=True, justify="center" ) table.add_column( - "Coldkey", style="bright_magenta", no_wrap=True, justify="center" + "Coldkey", style="plum2", no_wrap=True, justify="center" ) for row in rows: table.add_row(*row) @@ -487,7 +487,7 @@ async def show_subnet(netuid_: int): console.print(table) console.print("\n") console.print( - f"Subnet: {netuid_}:\n Owner: [bold bright_magenta]{subnet_info.owner}[/bold bright_magenta]\n Total Locked: [green]{subnet_info.total_locked}[/green]\n Owner Locked: [green]{subnet_info.owner_locked}[/green]" + f"Subnet: {netuid_}:\n Owner: [bold plum2]{subnet_info.owner}[/bold plum2]\n Total Locked: [dark_sea_green]{subnet_info.total_locked}[/dark_sea_green]\n Owner Locked: [dark_sea_green]{subnet_info.owner_locked}[/dark_sea_green]" ) console.print( """ @@ -631,7 +631,7 @@ async def register( ) table.add_column( f"Cost ({Balance.get_unit(0)})", - style="light_goldenrod2", + style="tan", no_wrap=True, justify="center", ) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 641fd7ba..36bab3a1 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -496,11 +496,11 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): subnet = await subtensor.get_subnet_hyperparameters(netuid) table = Table( - Column("[white]HYPERPARAMETER", style="bright_magenta"), + Column("[white]HYPERPARAMETER", style="plum2"), Column("[white]VALUE", style="light_goldenrod2"), Column("[white]NORMALIZED", style="light_goldenrod3"), - title=f"[underline dark_orange]\nSubnet Hyperparameters[/underline dark_orange]\n NETUID: [dark_orange]" - f"{netuid}[/dark_orange] - Network: [dark_orange]{subtensor.network}[/dark_orange]\n", + title=f"[underline navajo_white1]\nSubnet Hyperparameters[/underline navajo_white1]\n NETUID: [navajo_white1]" + f"{netuid}[/navajo_white1] - Network: [navajo_white1]{subtensor.network}[/navajo_white1]\n", show_footer=True, width=None, pad_edge=False, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 0efe8dc1..ce504118 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -269,13 +269,13 @@ async def wallet_balance( ), Column( "[white]Coldkey Address", - style="bright_magenta", + style="plum2", no_wrap=True, ), Column( "[white]Free Balance", justify="right", - style="light_goldenrod2", + style="tan", no_wrap=True, ), Column( @@ -287,10 +287,10 @@ async def wallet_balance( Column( "[white]Total Balance", justify="right", - style="green", + style="dark_sea_green", no_wrap=True, ), - title=f"[underline dark_orange]Wallet Coldkey Balance[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}", + title=f"\n[underline navajo_white1]Wallet Coldkey Balance[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}", show_footer=True, show_edge=False, border_style="bright_black", From c5a803a615a5b875f14920d1e8b3ae2e8b766e7e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 14:40:06 -0700 Subject: [PATCH 104/332] Adds support for coldkey_ss58 in btcli st list --- bittensor_cli/cli.py | 31 ++++++++++++++++++++--- bittensor_cli/src/commands/stake/stake.py | 18 ++++++------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 159619a4..286108ea 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2417,17 +2417,40 @@ def stake_list( wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, + coldkey_ss58=typer.Option( + None, + "--ss58", + "--coldkey_ss58", + "--coldkey.ss58_address", + "--coldkey.ss58", + help="Coldkey address of the wallet", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, # TODO add: all-wallets, reuse_last, html_output ): """List all stake accounts for wallet.""" self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) + + wallet = None + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error("You entered an invalid ss58 address") + raise typer.Exit() + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue] [Press Enter to use config values]", + ) + if is_valid_ss58_address(coldkey_or_ss58): + coldkey_ss58 = coldkey_or_ss58 + else: + wallet_name = coldkey_or_ss58 if coldkey_or_ss58 else wallet_name + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + return self._run_command( - stake.stake_list(wallet, self.initialize_chain(network)) + stake.stake_list(wallet, coldkey_ss58, self.initialize_chain(network)) ) def stake_add( diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 8f442158..06ab4ef9 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1501,12 +1501,14 @@ async def unstake( console.print("[green]Unstaking operations completed.[/green]") -async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): +async def stake_list( + wallet: Wallet, coldkey_ss58: str, subtensor: "SubtensorInterface" +): + coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address + sub_stakes = ( - await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address] - ) - )[wallet.coldkeypub.ss58_address] + await subtensor.get_stake_info_for_coldkeys(coldkey_ss58_list=[coldkey_address]) + )[coldkey_address] # Get registered delegates details. registered_delegate_info = await subtensor.get_delegate_identities() @@ -1516,9 +1518,7 @@ async def stake_list(wallet: Wallet, subtensor: "SubtensorInterface"): emission_drain_tempo = int( await subtensor.substrate.query("SubtensorModule", "HotkeyEmissionTempo") ) - balance = (await subtensor.get_balance(wallet.coldkeypub.ss58_address))[ - wallet.coldkeypub.ss58_address - ] + balance = (await subtensor.get_balance(coldkey_address))[coldkey_address] # Iterate over substakes and aggregate them by hotkey. hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} @@ -1680,7 +1680,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): console.print("\n\n") console.print( f"Wallet:\n" - f" Coldkey SS58: [bold plum2]{wallet.coldkeypub.ss58_address}[/bold plum2]\n" + f" Coldkey SS58: [bold plum2]{coldkey_address}[/bold plum2]\n" f" Free Balance: [dark_sea_green]{balance}[/dark_sea_green]\n" f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" From e259e095ec78a1d6b1aa86c7b998d71519064322 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 30 Oct 2024 00:07:09 +0200 Subject: [PATCH 105/332] Update requirements from staging --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c3a5dfc2..2e891bcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ wheel async-property==0.2.2 aiohttp~=3.10.2 +backoff~=2.2.1 GitPython>=3.0.0 fuzzywuzzy~=0.18.0 netaddr~=1.3.0 From c57b98f335aaaeb51e3a7a596ae1ac04899a3216 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 17:34:00 -0700 Subject: [PATCH 106/332] Fixes order of prompts in st add, remove and alligns prompt color scheme --- bittensor_cli/cli.py | 50 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 286108ea..5d27f932 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1166,13 +1166,9 @@ def wallet_ask( f"Using the wallet name from config:[bold cyan] {wallet_name}" ) else: - wallet_name = typer.prompt( - typer.style("Enter the wallet name", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-name`)", - fg="green", - italic=True, - ), + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]" + + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-name`)[/dark_sea_green3 italic]", default=defaults.wallet.name, ) @@ -1183,13 +1179,9 @@ def wallet_ask( f"Using the wallet hotkey from config:[bold cyan] {wallet_hotkey}" ) else: - wallet_hotkey = typer.prompt( - typer.style("Enter the wallet hotkey", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-hotkey`)", - fg="green", - italic=True, - ), + wallet_hotkey = Prompt.ask( + "Enter the [blue]wallet hotkey[/blue]" + + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-hotkey`)[/dark_sea_green3 italic]", default=defaults.wallet.hotkey, ) if wallet_path: @@ -1203,13 +1195,9 @@ def wallet_ask( ) if WO.PATH in ask_for and not wallet_path: - wallet_path = typer.prompt( - typer.style("Enter the wallet path", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-path`)", - fg="green", - italic=True, - ), + wallet_path = Prompt.ask( + "Enter the [blue]wallet path[/blue]" + + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-path`)[/dark_sea_green3 italic]", default=defaults.wallet.path, ) # Create the Wallet object @@ -2439,7 +2427,8 @@ def stake_list( raise typer.Exit() else: coldkey_or_ss58 = Prompt.ask( - "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue] [Press Enter to use config values]", + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, ) if is_valid_ss58_address(coldkey_or_ss58): coldkey_ss58 = coldkey_or_ss58 @@ -2538,11 +2527,16 @@ def stake_add( raise typer.Exit() if not wallet_hotkey and not all_hotkeys and not include_hotkeys: + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [Press Enter to use config values]", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) if is_valid_ss58_address(hotkey_or_ss58): - hotkey_ss58_address = hotkey_or_ss58 wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) @@ -2727,8 +2721,14 @@ def stake_remove( and not all_hotkeys and not include_hotkeys ): + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from. [Press Enter to use config values]" + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) if is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 From ed6b2f80d38959bfd2746cd95a50380542ff1fd7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 22:49:43 -0700 Subject: [PATCH 107/332] optimize st remove --- bittensor_cli/src/commands/stake/stake.py | 46 ++++++++++++++--------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 06ab4ef9..5a87a495 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1212,13 +1212,32 @@ async def unstake( assert wallet.hotkey is not None hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] - # Prepare lists to store unstaking data per subnet - unstake_operations = [] - total_received_amount = Balance.from_tao(0) - current_wallet_balance: Balance = ( - await subtensor.get_balance(wallet.coldkeypub.ss58_address) - )[wallet.coldkeypub.ss58_address] - max_float_slippage = 0 + with console.status( + f"Retrieving stake and subnet data from {subtensor.network}...", + spinner="earth", + ): + # Prepare lists to store unstaking data per subnet + unstake_operations = [] + total_received_amount = Balance.from_tao(0) + current_wallet_balance: Balance = ( + await subtensor.get_balance(wallet.coldkeypub.ss58_address) + )[wallet.coldkeypub.ss58_address] + max_float_slippage = 0 + + # Fetch dynamic info and stake balances + chain_head = await subtensor.substrate.get_chain_head() + dynamic_info_list, stake_balances_dict = await asyncio.gather( + asyncio.gather( + *[subtensor.get_subnet_dynamic_info(x, chain_head) for x in netuids] + ), + subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( + hotkey_ss58s=[hk[1] for hk in hotkeys_to_unstake_from], + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuids=netuids, + block_hash=chain_head, + ), + ) + dynamic_info_all_netuids = dict(zip(netuids, dynamic_info_list)) # Iterate over hotkeys and netuids to collect unstake operations for hotkey in hotkeys_to_unstake_from: @@ -1235,19 +1254,12 @@ async def unstake( if skip_remaining_subnets: break # Exit the loop over netuids - # Check that the subnet exists. - dynamic_info = await subtensor.get_subnet_dynamic_info(netuid) + dynamic_info = dynamic_info_all_netuids.get(netuid) if dynamic_info is None: console.print(f"[red]Subnet: {netuid} does not exist.[/red]") continue # Skip to the next subnet - current_stake_balance: Balance = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=netuid, - ) - ) + current_stake_balance = stake_balances_dict[staking_address_ss58][netuid] if current_stake_balance.tao == 0: continue # No stake to unstake @@ -1369,7 +1381,7 @@ async def unstake( # Build the table table = Table( - title=f"\n[navajo_white1]Unstaking to: \nWallet: [dark_sea_green3]{wallet.name}[/dark_sea_green3], Coldkey ss58: [dark_sea_green3]{wallet.coldkeypub.ss58_address}[/dark_sea_green3]\nNetwork: {subtensor.network}[/navajo_white1]\n", + title=f"\n[tan]Unstaking to: \nWallet: [dark_sea_green3]{wallet.name}[/dark_sea_green3], Coldkey ss58: [dark_sea_green3]{wallet.coldkeypub.ss58_address}[/dark_sea_green3]\nNetwork: {subtensor.network}[/tan]\n", show_footer=True, show_edge=False, header_style="bold white", From d0840f4be28329f6b7dabfcd3fcbd227866a32d9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 29 Oct 2024 22:52:43 -0700 Subject: [PATCH 108/332] improved var --- bittensor_cli/src/commands/stake/stake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 5a87a495..2b8caf42 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1226,7 +1226,7 @@ async def unstake( # Fetch dynamic info and stake balances chain_head = await subtensor.substrate.get_chain_head() - dynamic_info_list, stake_balances_dict = await asyncio.gather( + dynamic_info_list, stake_all_netuids = await asyncio.gather( asyncio.gather( *[subtensor.get_subnet_dynamic_info(x, chain_head) for x in netuids] ), @@ -1259,7 +1259,7 @@ async def unstake( console.print(f"[red]Subnet: {netuid} does not exist.[/red]") continue # Skip to the next subnet - current_stake_balance = stake_balances_dict[staking_address_ss58][netuid] + current_stake_balance = stake_all_netuids[staking_address_ss58][netuid] if current_stake_balance.tao == 0: continue # No stake to unstake From 2dc7135cf2e665e77bfb172c65e515344cc52afd Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 30 Oct 2024 15:31:35 +0200 Subject: [PATCH 109/332] Clean up error handling for main CLI. Add KeyboardInterrupt. --- bittensor_cli/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5d27f932..42afda91 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -818,23 +818,23 @@ def _run_command(self, cmd: Coroutine) -> None: """ async def _run(): + run_cmd = asyncio.create_task(cmd) try: if self.subtensor: async with self.subtensor: - result = await cmd + result = await run_cmd else: - result = await cmd + result = await run_cmd return result except (ConnectionRefusedError, ssl.SSLError): err_console.print(f"Unable to connect to the chain: {self.subtensor}") - asyncio.create_task(cmd).cancel() raise typer.Exit() - except ConnectionClosed: - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except SubstrateRequestException as e: - err_console.print(str(e)) + except (ConnectionClosed, SubstrateRequestException, KeyboardInterrupt) as e: + if isinstance(e, SubstrateRequestException): + err_console.print(str(e)) raise typer.Exit() + finally: + run_cmd.cancel() if sys.version_info < (3, 10): # For Python 3.9 or lower From 5ae34100a3bd8b05f6018d5bef8054b4ff5bf28f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 09:23:02 -0700 Subject: [PATCH 110/332] Reverts changes to _run() --- bittensor_cli/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 42afda91..5d27f932 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -818,23 +818,23 @@ def _run_command(self, cmd: Coroutine) -> None: """ async def _run(): - run_cmd = asyncio.create_task(cmd) try: if self.subtensor: async with self.subtensor: - result = await run_cmd + result = await cmd else: - result = await run_cmd + result = await cmd return result except (ConnectionRefusedError, ssl.SSLError): err_console.print(f"Unable to connect to the chain: {self.subtensor}") + asyncio.create_task(cmd).cancel() raise typer.Exit() - except (ConnectionClosed, SubstrateRequestException, KeyboardInterrupt) as e: - if isinstance(e, SubstrateRequestException): - err_console.print(str(e)) + except ConnectionClosed: + asyncio.create_task(cmd).cancel() + raise typer.Exit() + except SubstrateRequestException as e: + err_console.print(str(e)) raise typer.Exit() - finally: - run_cmd.cancel() if sys.version_info < (3, 10): # For Python 3.9 or lower From b51f2bdd1af5073ce5502635db38f130ae144cbe Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 10:13:02 -0700 Subject: [PATCH 111/332] Adds hidden for overview, inspect --- bittensor_cli/cli.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5d27f932..d1022138 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -622,15 +622,19 @@ def __init__(self): self.wallet_app.command( "history", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] )(self.wallet_history) - # self.wallet_app.command( - # "overview", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] - # )(self.wallet_overview) + self.wallet_app.command( + "overview", + rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], + hidden=True, + )(self.wallet_overview) self.wallet_app.command( "transfer", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_transfer) - # self.wallet_app.command( - # "inspect", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] - # )(self.wallet_inspect) + self.wallet_app.command( + "inspect", + rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], + hidden=True, + )(self.wallet_inspect) self.wallet_app.command( "faucet", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_faucet) From 14ff0fd3d44b0e0d19ec3e5b6e939fdde104c41e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 30 Oct 2024 21:23:11 +0200 Subject: [PATCH 112/332] Fix taostats link --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index e63807a8..e65f665d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -443,7 +443,7 @@ def get_explorer_url_for_network( explorer_opentensor_url = "{root_url}/query/{block_hash}".format( root_url=explorer_root_urls.get("opentensor"), block_hash=block_hash ) - explorer_taostats_url = "{root_url}/extrinsic/{block_hash}".format( + explorer_taostats_url = "{root_url}/hash/{block_hash}".format( root_url=explorer_root_urls.get("taostats"), block_hash=block_hash ) explorer_urls["opentensor"] = explorer_opentensor_url From 95ecb6f1905439b1f8efb315aac7b67243b42121 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 14:42:42 -0700 Subject: [PATCH 113/332] updated btcli s list --- bittensor_cli/src/commands/subnets.py | 30 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index e311b8fd..3884f490 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -179,12 +179,13 @@ async def subnets_list( ( str(netuid), f"[light_goldenrod1]{subnet.symbol}[light_goldenrod1]", - f"τ {emission_tao:.4f}", - f"τ {subnet.tao_in.tao:,.4f}", - f"{subnet.alpha_out.tao:,.4f} {symbol}", - f"{subnet.price.tao:.4f} τ/{symbol}", - f"{subnet.blocks_since_last_step}/{subnet.tempo}", - f"{global_weight:.4f}" if global_weight is not None else "N/A", + f"τ {emission_tao:.4f}", # Emission (t) + f"{subnet.alpha_out.tao:,.4f} {symbol}", # Stake a_out + f"τ {subnet.tao_in.tao:,.4f}", # TAO Pool t_in + f"{subnet.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in + f"{subnet.price.tao:.4f} τ/{symbol}", # Rate t_in/a_in + f"{subnet.blocks_since_last_step}/{subnet.tempo}", # Tempo k/n + f"{global_weight:.4f}" if global_weight is not None else "N/A", # Local weight coeff. (γ) ) ) total_emissions = sum( @@ -212,17 +213,22 @@ async def subnets_list( footer=f"τ {total_emissions:.4f}", ) table.add_column( - f"[bold white]TAO ({Balance.get_unit(0)})", + f"[bold white]STAKE ({Balance.get_unit(1)}_out)", + style="light_salmon3", + justify="right", + ) + table.add_column( + f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", style="rgb(42,161,152)", justify="right", ) table.add_column( - f"[bold white]STAKE ({Balance.get_unit(1)})", - style="light_salmon3", + f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", + style="rgb(42,161,152)", justify="right", ) table.add_column( - f"[bold white]RATE ({Balance.get_unit(1)}/{Balance.get_unit(0)})", + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="medium_purple", justify="right", ) @@ -232,7 +238,7 @@ async def subnets_list( justify="right", overflow="fold", ) - table.add_column("[bold white]Global weight (γ)", style="dark_sea_green3", justify="center") + table.add_column("[bold white]Local weight coeff. (γ)", style="dark_sea_green3", justify="center") # Sort rows by subnet.emission.tao, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) @@ -244,7 +250,7 @@ async def subnets_list( # Print the table console.print(table) - # TODO: Add description for global weights + console.print( """ [bold white]Description[/bold white]: From 2cd8184f5007c92ea39c3b3d597a8d3d00593ffd Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 16:21:41 -0700 Subject: [PATCH 114/332] Updates description of subnet list --- bittensor_cli/src/commands/subnets.py | 103 ++++++++++++++++++-------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 3884f490..a0a9ab9c 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -2,7 +2,13 @@ import json import sqlite3 from textwrap import dedent +from rich.panel import Panel +from rich.markdown import Markdown +from rich.text import Text +from rich.style import Style +import textwrap from typing import TYPE_CHECKING, Optional, cast +from rich import box from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError @@ -179,13 +185,15 @@ async def subnets_list( ( str(netuid), f"[light_goldenrod1]{subnet.symbol}[light_goldenrod1]", - f"τ {emission_tao:.4f}", # Emission (t) - f"{subnet.alpha_out.tao:,.4f} {symbol}", # Stake a_out - f"τ {subnet.tao_in.tao:,.4f}", # TAO Pool t_in - f"{subnet.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in - f"{subnet.price.tao:.4f} τ/{symbol}", # Rate t_in/a_in - f"{subnet.blocks_since_last_step}/{subnet.tempo}", # Tempo k/n - f"{global_weight:.4f}" if global_weight is not None else "N/A", # Local weight coeff. (γ) + f"τ {emission_tao:.4f}", # Emission (t) + f"{subnet.alpha_out.tao:,.4f} {symbol}", # Stake a_out + f"τ {subnet.tao_in.tao:,.4f}", # TAO Pool t_in + f"{subnet.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in + f"{subnet.price.tao:.4f} τ/{symbol}", # Rate t_in/a_in + f"{subnet.blocks_since_last_step}/{subnet.tempo}", # Tempo k/n + f"{global_weight:.4f}" + if global_weight is not None + else "N/A", # Local weight coeff. (γ) ) ) total_emissions = sum( @@ -238,7 +246,9 @@ async def subnets_list( justify="right", overflow="fold", ) - table.add_column("[bold white]Local weight coeff. (γ)", style="dark_sea_green3", justify="center") + table.add_column( + "[bold white]Local weight coeff. (γ)", style="dark_sea_green3", justify="center" + ) # Sort rows by subnet.emission.tao, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) @@ -250,22 +260,55 @@ async def subnets_list( # Print the table console.print(table) - - console.print( - """ + header = """ [bold white]Description[/bold white]: -The table displays relevant information about each subnet on the network. +The table displays relevant information about each subnet on the network. The columns are as follows: - - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). - - [bold white]Symbol[/bold white]: The symbol representing the subnet's stake. - - [bold white]Emission[/bold white]: The amount of TAO added to the subnet every block. Calculated by dividing the TAO (t) column values by the sum of the TAO (t) column. - - [bold white]TAO[/bold white]: The TAO staked into the subnet ( which dynamically changes during stake, unstake and emission events ). - - [bold white]Stake[/bold white]: The outstanding supply of stake across all staking accounts on this subnet. - - [bold white]Rate[/bold white]: The rate of conversion between TAO and the subnet's staking unit. - - [bold white]Tempo[/bold white]: The number of blocks between epochs. Represented as (k/n) where k is the blocks since the last epoch and n is the total blocks in the epoch. - - [bold white]Global weight[/bold white]: The global weight of the subnet across all subnets. """ - ) + console.print(header) + description_table = Table(show_header=False, box=None, show_edge=False, leading=2) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet (its index).\n"), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.\n", + ), + ( + "[bold tan]Emission (τ)[/bold tan]", + "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + ), + ( + "[bold tan]STAKE (α_out)[/bold tan]", + "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + ), + ( + "[bold tan]TAO Pool (τ_in)[/bold tan]", + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n', + ), + ( + "[bold tan]Alpha Pool (α_in)[/bold tan]", + "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + ), + ( + "[bold tan]Tempo (k/n)[/bold tan]", + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n', + ), + ( + "[bold tan]Local weight coeff (γ)[/bold tan]", + "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + ), + ] + + description_table.add_column("Field", no_wrap=True, style="bold tan") + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def show(subtensor: "SubtensorInterface", netuid: int, prompt: bool = True): @@ -461,9 +504,15 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="center", ) - table.add_column("Dividends", style="#8787d7", no_wrap=True, justify="center", footer=f"{relative_emissions_sum:.3f}",) + table.add_column( + "Dividends", + style="#8787d7", + no_wrap=True, + justify="center", + footer=f"{relative_emissions_sum:.3f}", + ) table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") - + # Hiding relative emissions for now # table.add_column( # "Emissions", @@ -479,12 +528,8 @@ async def show_subnet(netuid_: int): justify="center", footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), ) - table.add_column( - "Hotkey", style="plum2", no_wrap=True, justify="center" - ) - table.add_column( - "Coldkey", style="plum2", no_wrap=True, justify="center" - ) + table.add_column("Hotkey", style="plum2", no_wrap=True, justify="center") + table.add_column("Coldkey", style="plum2", no_wrap=True, justify="center") for row in rows: table.add_row(*row) From 8bf0d391e79c90676149e9ce0911d9afad270a59 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 16:22:52 -0700 Subject: [PATCH 115/332] Removes extra imports --- bittensor_cli/src/commands/subnets.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index a0a9ab9c..af0b35c0 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -1,14 +1,7 @@ import asyncio import json import sqlite3 -from textwrap import dedent -from rich.panel import Panel -from rich.markdown import Markdown -from rich.text import Text -from rich.style import Style -import textwrap from typing import TYPE_CHECKING, Optional, cast -from rich import box from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError From 4f483b17c2984d2441489d5f138905895bc54ab6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 16:38:13 -0700 Subject: [PATCH 116/332] tweaks to s list description --- bittensor_cli/src/commands/subnets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index af0b35c0..a0b79f5b 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -254,15 +254,13 @@ async def subnets_list( console.print(table) header = """ -[bold white]Description[/bold white]: -The table displays relevant information about each subnet on the network. -The columns are as follows: +[bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: """ console.print(header) description_table = Table(show_header=False, box=None, show_edge=False, leading=2) fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet (its index).\n"), + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet.\n"), ( "[bold tan]Symbol[/bold tan]", "The symbol for the subnet's dynamic TAO token.\n", From 70d8405c0e136e0c53df98ea94c2e4385b45bea9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 17:52:04 -0700 Subject: [PATCH 117/332] Updates btcli st list table --- bittensor_cli/src/commands/stake/stake.py | 148 ++++++++++++++++------ 1 file changed, 107 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2b8caf42..064b6464 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -8,6 +8,7 @@ from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Table +from rich import box from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance @@ -1543,8 +1544,9 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): else hotkey_ ) rows = [] - total_global_tao = Balance(0) + total_tao_ownership = Balance(0) total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) for substake_ in substakes: netuid = substake_.netuid pool = dynamic_info[netuid] @@ -1563,6 +1565,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( substake_.stake ) + total_swapped_tao_value += swapped_tao_value if pool.is_dynamic: slippage_percentage_ = ( 100 * float(slippage) / float(slippage + swapped_tao_value) @@ -1583,7 +1586,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): tao_ownership = Balance.from_tao( (alpha_value.tao / issuance.tao) * tao_locked.tao ) - total_global_tao += tao_ownership + total_tao_ownership += tao_ownership else: # TODO what's this var for? alpha_ownership = "0.0000" @@ -1592,22 +1595,20 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): [ str(netuid), # Number symbol, # Symbol - # f"[medium_purple]{tao_ownership}[/medium_purple] ([light_salmon3]{ alpha_ownership }[/light_salmon3][white]%[/white])", # Tao ownership. - f"[medium_purple]{tao_ownership}[/medium_purple]", # Tao ownership. - # f"[dark_sea_green]{ alpha_value }", # Alpha value - f"{substake_.stake.tao:,.4f} {symbol}", - f"{pool.price.tao:.4f} τ/{symbol}", - f"[light_slate_blue]{tao_value}[/light_slate_blue]", # Tao equiv - f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap amount. - # f"[light_salmon3]{ alpha_ownership }%[/light_salmon3]", # Ownership. + f"{substake_.stake.tao:,.4f} {symbol}", # Stake (a) + f"{pool.tao_in.tao:,.4f} τ", # TAO pool (t_in) + f"{pool.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in + f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) + f"{pool.alpha_out.tao:,.4f} {symbol}", # Alpha out (a_out) + f"[medium_purple]{tao_ownership}[/medium_purple]", # TAO equiv + f"[light_slate_blue]{tao_value}[/light_slate_blue]", # Exchange Value (α x τ/α) + f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap(α) -> τ "[bold cadet_blue]YES[/bold cadet_blue]" if substake_.is_registered - else "[dark_red]NO[/dark_red]", - # Registered. + else "[dark_red]NO[/dark_red]", # Registered str(Balance.from_tao(per_block_emission).set_unit(netuid)) if substake_.is_registered - else "[dark_red]N/A[/dark_red]", # emission per block. - # f"[light_slate_blue]{locked_value}[/light_slate_blue]", # Locked value + else "[dark_red]N/A[/dark_red]", # Emission(α/block) ] ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") @@ -1631,47 +1632,62 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): no_wrap=True, ) table.add_column( - f"[white]TAO({Balance.unit})", + f"[white]Stake ({Balance.get_unit(1)})", + footer_style="overline white", + style="rgb(42,161,152)", + justify="center", + ) + table.add_column( + f"[white]TAO pool ({Balance.unit}_in)", style="medium_purple", justify="right", - footer=f"{total_global_tao}", ) table.add_column( - f"[white]Stake({Balance.get_unit(1)})", - footer_style="overline white", - style="rgb(42,161,152)", - justify="center", + f"[white]Alpha pool ({Balance.get_unit(1)}_in)", + style="medium_purple", + justify="right", ) table.add_column( - f"[white]Rate({Balance.unit}/{Balance.get_unit(1)})", + f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", footer_style="white", style="light_goldenrod2", justify="center", ) table.add_column( - f"[white]Value({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + f"[white]Alpha out ({Balance.get_unit(1)}_out)", + style="medium_purple", + justify="right", + ) + table.add_column( + f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", + style="medium_purple", + justify="right", + footer=f"{total_tao_ownership}", + ) + table.add_column( + f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", footer_style="overline white", style="blue", justify="right", footer=f"{total_tao_value}", ) table.add_column( - f"[white]Swap({Balance.get_unit(1)}) -> {Balance.unit}", + f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", footer_style="overline white", style="white", justify="right", + footer=f"{total_swapped_tao_value}", ) - # table.add_column(f"[white]Control({bittensor.Balance.get_unit(1)})", style="aquamarine3", justify="right") table.add_column("[white]Registered", style="red", justify="right") table.add_column( - f"[white]Emission({Balance.get_unit(1)}/block)", + f"[white]Emission \n({Balance.get_unit(1)}/block)", style="light_goldenrod2", justify="right", ) for row in rows: table.add_row(*row) console.print(table) - return total_global_tao, total_tao_value + return total_tao_ownership, total_tao_value for substake in sub_stakes: hotkey = substake.hotkey_ss58 @@ -1697,23 +1713,73 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" ) - console.print( - """ -[bold white]Description[/bold white]: -Each table displays information about your coldkey's staking accounts with a hotkey. -The header of the table displays the hotkey and the footer displays the total stake and total value of all your staking accounts. -The columns of the table are as follows: - - [bold white]Netuid[/bold white]: The unique identifier for the subnet (its index). - - [bold white]Symbol[/bold white]: The symbol representing the subnet stake's unit. - - [bold white]TAO[/bold white]: The hotkey's TAO balance on this subnet. This is this hotkey's proportion of total TAO staked into the subnet divided by the hotkey's share of outstanding stake. - - [bold white]Stake[/bold white]: The hotkey's stake balance in subnets staking unit. - - [bold white]Rate[/bold white]: The rate of exchange between the subnet's staking unit and the subnet's TAO. - - [bold white]Value[/bold white]: The price of the hotkey's stake in TAO computed via the exchange rate. - - [bold white]Swap[/bold white]: The amount of TAO received when unstaking all of the hotkey's stake (with slippage). - - [bold white]Registered[/bold white]: Whether the hotkey is registered on this subnet. - - [bold white]Emission[/bold white]: If registered, the emission (in stake) attained by this hotkey on this subnet per block. + header = """ +[bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: """ + console.print(header) + description_table = Table(show_header=False, box=None, show_edge=False) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Emission (τ)[/bold tan]", + "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]Stake (α)[/bold tan]", + "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]TAO Pool (τ_in)[/bold tan]", + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + ), + ( + "[bold tan]Alpha Pool (α_in)[/bold tan]", + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]Alpha out (α_out)[/bold tan]", + "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + ), + ( + "[bold tan]Exchange Value (α x τ/α)[/bold tan]", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]Swap (α → τ)[/bold tan]", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]Registered[/bold tan]", + "Indicates if the hotkey is registered in this subnet or not. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ( + "[bold tan]Emission (α/block)[/bold tan]", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + ), + ] + + description_table.add_column( + "Field", + no_wrap=True, + style="bold tan", ) + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def move_stake( From feb88400b1a40a727d417ece147874087511bfba Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 17:57:23 -0700 Subject: [PATCH 118/332] tweaks description of btcli st list --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 064b6464..f3a9f908 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1717,7 +1717,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: """ console.print(header) - description_table = Table(show_header=False, box=None, show_edge=False) + description_table = Table(show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True) fields = [ ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), From 54cc6aed56806b535eff1fca28be67c3739e2e5d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 18:03:31 -0700 Subject: [PATCH 119/332] Fixes dupe emissions desc --- bittensor_cli/src/commands/stake/stake.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index f3a9f908..a0760c05 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1717,7 +1717,9 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: """ console.print(header) - description_table = Table(show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) fields = [ ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), @@ -1725,10 +1727,6 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): "[bold tan]Symbol[/bold tan]", "The symbol for the subnet's dynamic TAO token.", ), - ( - "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", - ), ( "[bold tan]Stake (α)[/bold tan]", "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", From 6e521c5e016a991a8a4e5878ff7f49c3d22cab2e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 18:30:09 -0700 Subject: [PATCH 120/332] Exposes few urls in st list --- bittensor_cli/src/commands/stake/stake.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index a0760c05..bb8769a1 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1729,19 +1729,19 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]Stake (α)[/bold tan]", - "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]Alpha Pool (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Alpha out (α_out)[/bold tan]", From 97ce7662a4066384fda5fe5d9e8f93e8b04f480e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 18:31:15 -0700 Subject: [PATCH 121/332] line breaks in desc st list --- bittensor_cli/src/commands/stake/stake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index bb8769a1..df92dbde 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1737,11 +1737,11 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]Alpha Pool (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. For more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Alpha out (α_out)[/bold tan]", From a9426d61c83756452b6e249417eec956bf28a16f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 19:01:43 -0700 Subject: [PATCH 122/332] updates hyperlinks --- bittensor_cli/src/commands/stake/stake.py | 18 ++++++++-------- bittensor_cli/src/commands/subnets.py | 25 +++++++++++++---------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index df92dbde..0c66b089 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1605,15 +1605,15 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap(α) -> τ "[bold cadet_blue]YES[/bold cadet_blue]" if substake_.is_registered - else "[dark_red]NO[/dark_red]", # Registered + else "[hot_pink3]NO[/hot_pink3]", # Registered str(Balance.from_tao(per_block_emission).set_unit(netuid)) if substake_.is_registered - else "[dark_red]N/A[/dark_red]", # Emission(α/block) + else "[hot_pink3]N/A[/hot_pink3]", # Emission(α/block) ] ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") table = Table( - title=f"\n[dark_orange]Hotkey: {name}[/dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", + title=f"\n[dark_orange]Hotkey: {name}[/dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n\nSee below for an explanation of the columns\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1745,27 +1745,27 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].', + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Registered[/bold tan]", - "Indicates if the hotkey is registered in this subnet or not. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ] diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index a0b79f5b..4a42642b 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -7,6 +7,7 @@ from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm from rich.table import Column, Table +from rich import box from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetState @@ -194,7 +195,7 @@ async def subnets_list( ) table = Table( - title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n", + title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n\nSee below for an explanation of the columns\n", show_footer=True, show_edge=False, header_style="bold white", @@ -257,41 +258,43 @@ async def subnets_list( [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: """ console.print(header) - description_table = Table(show_header=False, box=None, show_edge=False, leading=2) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet.\n"), + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), ( "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.\n", + "The symbol for the subnet's dynamic TAO token.", ), ( "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n', + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]Alpha Pool (α_in)[/bold tan]", - "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Tempo (k/n)[/bold tan]", - 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n', + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]Local weight coeff (γ)[/bold tan]", - "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. For more, see [link=https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo][blue]URL[/blue][/link].\n", + "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ] From 6145d81a708d6c049733cc9d97f20c652e4e9b50 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 19:21:41 -0700 Subject: [PATCH 123/332] Adds prompts in stake list --- bittensor_cli/src/commands/stake/stake.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 0c66b089..47e7f40c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1698,13 +1698,20 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): hotkeys_to_substakes[hotkey].append(substake) # Iterate over each hotkey and make a table + counter = 0 + num_hotkeys = len(hotkeys_to_substakes) all_hotkeys_total_global_tao = Balance(0) all_hotkeys_total_tao_value = Balance(0) for hotkey in hotkeys_to_substakes.keys(): + counter += 1 stake, value = table_substakes(hotkey, hotkeys_to_substakes[hotkey]) all_hotkeys_total_global_tao += stake all_hotkeys_total_tao_value += value + if num_hotkeys > 1 and counter < num_hotkeys: + console.print("\nPress any key to continue to the next hotkey...") + input() + console.print("\n\n") console.print( f"Wallet:\n" @@ -1713,6 +1720,9 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" ) + + console.print("\nPress any key to continue to column descriptions...") + input() header = """ [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: """ From b9727d1a14e8a2e90945bbdc407ccb9a883c9f13 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 30 Oct 2024 19:27:15 -0700 Subject: [PATCH 124/332] Updates input text and adds prompt in s list --- bittensor_cli/src/commands/stake/stake.py | 4 ++-- bittensor_cli/src/commands/subnets.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 47e7f40c..33198509 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1709,7 +1709,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): all_hotkeys_total_tao_value += value if num_hotkeys > 1 and counter < num_hotkeys: - console.print("\nPress any key to continue to the next hotkey...") + console.print("\nPress Enter to continue to the next hotkey...") input() console.print("\n\n") @@ -1721,7 +1721,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" ) - console.print("\nPress any key to continue to column descriptions...") + console.print("\nPress Enter to continue to column descriptions...") input() header = """ [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 4a42642b..286ff7f5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -254,6 +254,8 @@ async def subnets_list( # Print the table console.print(table) + console.print("\nPress Enter to continue to column descriptions...") + input() header = """ [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: """ From 914cec6c5dc418850171a900f3fd5ad750e62ba8 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 1 Nov 2024 18:04:28 -0700 Subject: [PATCH 125/332] Added live view to subnets list --- bittensor_cli/cli.py | 7 + bittensor_cli/src/commands/subnets.py | 478 +++++++++++++++++++------- 2 files changed, 357 insertions(+), 128 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d1022138..ce74d1c7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -218,6 +218,11 @@ class Options: "--quiet", help="Display only critical information on the console.", ) + live = typer.Option( + False, + "--live", + help="Display live view of the table", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -3391,6 +3396,7 @@ def subnets_list( # html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + live_mode: bool = Options.live, ): """ List all subnets and their detailed information. @@ -3427,6 +3433,7 @@ def subnets_list( False, # reuse-last False, # html-output not self.config.get("use_cache", True), + live_mode, ) ) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 286ff7f5..7e5f7364 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -6,6 +6,10 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm +from rich.console import Console, Group +from rich.spinner import Spinner +from rich.text import Text +from rich.progress import Progress, BarColumn, TextColumn from rich.table import Column, Table from rich import box @@ -15,6 +19,7 @@ register_extrinsic, burned_register_extrinsic, ) +from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph from bittensor_cli.src.commands.wallets import set_id, set_id_prompts from bittensor_cli.src.bittensor.utils import ( @@ -154,151 +159,368 @@ async def _find_event_attributes_in_extrinsic_receipt( async def subnets_list( - subtensor: "SubtensorInterface", reuse_last: bool, html_output: bool, no_cache: bool + subtensor: "SubtensorInterface", + reuse_last: bool, + html_output: bool, + no_cache: bool, + live: bool, ): """List all subnet netuids in the network.""" - # TODO add reuse-last and html-output and no-cache - rows = [] - subnets = await subtensor.get_all_subnet_dynamic_info() - global_weights = await subtensor.get_global_weights( - [subnet.netuid for subnet in subnets] - ) + async def fetch_subnet_data(): + subnets = await subtensor.get_all_subnet_dynamic_info() + global_weights = await subtensor.get_global_weights( + [subnet.netuid for subnet in subnets] + ) + return subnets, global_weights - for subnet in subnets: - netuid = subnet.netuid - global_weight = global_weights.get(netuid) - symbol = f"{subnet.symbol}\u200e" + def define_table(total_emissions: float): + table = Table( + title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]" + f"\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) - if netuid == 0: - emission_tao = 0.0 - else: - emission_tao = subnet.emission.tao + table.add_column("[bold white]NETUID", style="white", justify="center") + table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") + table.add_column( + f"[bold white]EMISSION ({Balance.get_unit(0)})", + style="tan", + justify="left", + footer=f"τ {total_emissions:.4f}", + ) + table.add_column( + f"[bold white]STAKE ({Balance.get_unit(1)}_out)", + style="light_salmon3", + justify="left", + ) + table.add_column( + f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", + style="rgb(42,161,152)", + justify="left", + ) + table.add_column( + f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", + style="rgb(42,161,152)", + justify="left", + ) + table.add_column( + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style="medium_purple", + justify="left", + ) + table.add_column( + "[bold white]Tempo (k/n)", + style="plum2", + justify="left", + overflow="fold", + ) + table.add_column( + "[bold white]Local weight coeff. (γ)", style="steel_blue", justify="left" + ) + return table - rows.append( - ( - str(netuid), - f"[light_goldenrod1]{subnet.symbol}[light_goldenrod1]", - f"τ {emission_tao:.4f}", # Emission (t) - f"{subnet.alpha_out.tao:,.4f} {symbol}", # Stake a_out - f"τ {subnet.tao_in.tao:,.4f}", # TAO Pool t_in - f"{subnet.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in - f"{subnet.price.tao:.4f} τ/{symbol}", # Rate t_in/a_in - f"{subnet.blocks_since_last_step}/{subnet.tempo}", # Tempo k/n - f"{global_weight:.4f}" - if global_weight is not None - else "N/A", # Local weight coeff. (γ) + # Non-live mode + def create_table(subnets, global_weights): + rows = [] + for subnet in subnets: + netuid = subnet.netuid + global_weight = global_weights.get(netuid) + symbol = f"{subnet.symbol}\u200e" + + if netuid == 0: + emission_tao = 0.0 + else: + emission_tao = subnet.emission.tao + + # Prepare content + netuid_cell = str(netuid) + symbol_cell = f"{subnet.symbol}" + emission_cell = f"{emission_tao:,.4f}" + alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" + tao_in_cell = f"{subnet.tao_in.tao:,.4f} τ" + alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" + price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" + tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" + global_weight_cell = ( + f"{global_weight:.4f}" if global_weight is not None else "N/A" ) + + rows.append( + ( + netuid_cell, # Netuid + symbol_cell, # Symbol + emission_cell, # Emission (τ) + alpha_out_cell, # Stake α_out + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + price_cell, # Rate τ_in/α_in + tempo_cell, # Tempo k/n + global_weight_cell, # Local weight coeff. (γ) + ) + ) + + total_emissions = sum( + float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) - total_emissions = sum( - float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 - ) - table = Table( - title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n\nSee below for an explanation of the columns\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) + table = define_table(total_emissions) - table.add_column("[bold white]NETUID", style="white", justify="center") - table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") - table.add_column( - f"[bold white]EMISSION ({Balance.get_unit(0)})", - style="tan", - justify="right", - footer=f"τ {total_emissions:.4f}", - ) - table.add_column( - f"[bold white]STAKE ({Balance.get_unit(1)}_out)", - style="light_salmon3", - justify="right", - ) - table.add_column( - f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", - style="rgb(42,161,152)", - justify="right", - ) - table.add_column( - f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", - style="rgb(42,161,152)", - justify="right", - ) - table.add_column( - f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", - style="medium_purple", - justify="right", - ) - table.add_column( - "[bold white]Tempo (k/n)", - style="plum2", - justify="right", - overflow="fold", - ) - table.add_column( - "[bold white]Local weight coeff. (γ)", style="dark_sea_green3", justify="center" - ) + # Sort rows by emission, keeping the root subnet in the first position + sorted_rows = [rows[0]] + sorted( + rows[1:], key=lambda x: float(str(x[2]).replace(",", "")), reverse=True + ) + + # Add rows to the table + for row in sorted_rows: + table.add_row(*row) + return table + + # Live mode + def create_table_live(subnets, global_weights, previous_data): + def format_cell(value, previous_value, unit="", precision=4): + if previous_value is not None: + change = value - previous_value + if change > 0: + change_text = ( + f" [pale_green3](+{change:.{precision}f}{unit})[/pale_green3]" + ) + elif change < 0: + change_text = ( + f" [hot_pink3]({change:.{precision}f}{unit})[/hot_pink3]" + ) + else: + change_text = "" + else: + change_text = "" + return f"{value:,.{precision}f}{unit}{change_text}" + + rows = [] + current_data = {} # To store current values for comparison in the next update + + for subnet in subnets: + netuid = subnet.netuid + global_weight = global_weights.get(netuid) + symbol = f"{subnet.symbol}\u200e" + + if netuid == 0: + emission_tao = 0.0 + else: + emission_tao = subnet.emission.tao + + # Store current values for comparison + current_data[netuid] = { + "emission_tao": emission_tao, + "alpha_out": subnet.alpha_out.tao, + "tao_in": subnet.tao_in.tao, + "alpha_in": subnet.alpha_in.tao, + "price": subnet.price.tao, + "blocks_since_last_step": subnet.blocks_since_last_step, + "global_weight": global_weight, + } + + # Retrieve previous data if available + prev = previous_data.get(netuid) if previous_data else {} + + # Prepare content + netuid_cell = str(netuid) + symbol_cell = f"{subnet.symbol}" + emission_cell = format_cell( + emission_tao, prev.get("emission_tao"), unit="", precision=4 + ) + alpha_out_cell = format_cell( + subnet.alpha_out.tao, + prev.get("alpha_out"), + unit=f" {symbol}", + precision=5, + ) + tao_in_cell = format_cell( + subnet.tao_in.tao, prev.get("tao_in"), unit=" τ", precision=4 + ) + alpha_in_cell = format_cell( + subnet.alpha_in.tao, + prev.get("alpha_in"), + unit=f" {symbol}", + precision=4, + ) + price_cell = format_cell( + subnet.price.tao, prev.get("price"), unit=f" τ/{symbol}", precision=4 + ) - # Sort rows by subnet.emission.tao, keeping the first subnet in the first position - sorted_rows = [rows[0]] + sorted(rows[1:], key=lambda x: x[2], reverse=True) + # Content: Blocks_since_last_step + prev_blocks_since_last_step = prev.get("blocks_since_last_step") + if prev_blocks_since_last_step is not None: + if subnet.blocks_since_last_step >= prev_blocks_since_last_step: + block_change = ( + subnet.blocks_since_last_step - prev_blocks_since_last_step + ) + else: + # Tempo restarted + block_change = ( + subnet.blocks_since_last_step + subnet.tempo + 1 + ) - prev_blocks_since_last_step + if block_change > 0: + block_change_text = f" [pale_green3](+{block_change})[/pale_green3]" + elif block_change < 0: + block_change_text = f" [hot_pink3]({block_change})[/hot_pink3]" + else: + block_change_text = "" + else: + block_change_text = "" + tempo_cell = ( + f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" + ) - # Add rows to the table - for row in sorted_rows: - table.add_row(*row) + # Content: Global_weight + prev_global_weight = prev.get("global_weight") + if prev_global_weight is not None and global_weight is not None: + weight_change = float(global_weight) - float(prev_global_weight) + if weight_change > 0: + weight_change_text = ( + f" [pale_green3](+{weight_change:.6f})[/pale_green3]" + ) + elif weight_change < 0: + weight_change_text = ( + f" [hot_pink3]({weight_change:.6f})[/hot_pink3]" + ) + else: + weight_change_text = "" + else: + weight_change_text = "" + + global_weight_cell = ( + f"{global_weight:.4f}{weight_change_text}" + if global_weight is not None + else "N/A" + ) - # Print the table - console.print(table) + rows.append( + ( + netuid_cell, # Netuid + symbol_cell, # Symbol + emission_cell, # Emission (τ) + alpha_out_cell, # Stake α_out + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + price_cell, # Rate τ_in/α_in + tempo_cell, # Tempo k/n + global_weight_cell, # Local weight coeff. (γ) + ) + ) + + total_emissions = sum( + float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 + ) + table = define_table(total_emissions) + + # Sort rows by emission, keeping the first subnet in the first position + sorted_rows = [rows[0]] + sorted( + rows[1:], + key=lambda x: float(str(x[2]).split()[0].replace(",", "")), + reverse=True, + ) + + # Add rows to the table + for row in sorted_rows: + table.add_row(*row) + return table, current_data + + # Live mode + if live: + refresh_interval = 15 # seconds + + progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=20, style="green", complete_style="green"), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + console=console, + auto_refresh=True, + ) + progress_task = progress.add_task("Updating:", total=refresh_interval) + + previous_data = None + with Live(console=console, screen=True, auto_refresh=True) as live: + try: + while True: + subnets, global_weights = await fetch_subnet_data() + table, current_data = create_table_live( + subnets, global_weights, previous_data + ) + previous_data = current_data + progress.reset(progress_task) + start_time = asyncio.get_event_loop().time() + + # Create the message + message = "\nLive view active. Press [bold red]Ctrl + C[/bold red] to exit" + + # Include the message in the live render group + live_render = Group(table, progress, message) + live.update(live_render) + while not progress.finished: + await asyncio.sleep(0.1) + elapsed = asyncio.get_event_loop().time() - start_time + progress.update(progress_task, completed=elapsed) + + except KeyboardInterrupt: + pass # Ctrl + C + else: + # Non-live mode + subnets, global_weights = await fetch_subnet_data() + table = create_table(subnets, global_weights) + console.print(table) - console.print("\nPress Enter to continue to column descriptions...") - input() - header = """ + console.print("\nPress Enter to continue to column descriptions...") + input() + header = """ [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: """ - console.print(header) - description_table = Table( - show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True - ) + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) - fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), - ( - "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.", - ), - ( - "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Alpha Pool (α_in)[/bold tan]", - "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Tempo (k/n)[/bold tan]", - 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Local weight coeff (γ)[/bold tan]", - "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ] + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Emission (τ)[/bold tan]", + "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]STAKE (α_out)[/bold tan]", + "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Pool (τ_in)[/bold tan]", + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Alpha Pool (α_in)[/bold tan]", + "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Tempo (k/n)[/bold tan]", + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Local weight coeff (γ)[/bold tan]", + "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ] description_table.add_column("Field", no_wrap=True, style="bold tan") description_table.add_column("Description", overflow="fold") From ebb1ad9109eb5003fa7a03c4ed6a0f21108fe644 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 1 Nov 2024 18:11:52 -0700 Subject: [PATCH 126/332] Adds sum to Rate in subnets list --- bittensor_cli/src/commands/subnets.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 7e5f7364..48b07e0c 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -174,7 +174,7 @@ async def fetch_subnet_data(): ) return subnets, global_weights - def define_table(total_emissions: float): + def define_table(total_emissions: float, total_rate: float): table = Table( title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]" f"\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n\n", @@ -215,6 +215,7 @@ def define_table(total_emissions: float): f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="medium_purple", justify="left", + footer=f"τ {total_rate:.4f}", ) table.add_column( "[bold white]Tempo (k/n)", @@ -270,8 +271,10 @@ def create_table(subnets, global_weights): total_emissions = sum( float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) - - table = define_table(total_emissions) + total_rate = sum( + float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + ) + table = define_table(total_emissions, total_rate) # Sort rows by emission, keeping the root subnet in the first position sorted_rows = [rows[0]] + sorted( @@ -418,7 +421,10 @@ def format_cell(value, previous_value, unit="", precision=4): total_emissions = sum( float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) - table = define_table(total_emissions) + total_rate = sum( + float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + ) + table = define_table(total_emissions, total_rate) # Sort rows by emission, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted( From 79b2598e3ff655953923d3291b8ed4abd172da85 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 09:57:22 -0800 Subject: [PATCH 127/332] Adds verification for json file used in regenerating --- bittensor_cli/cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ce74d1c7..4f1ba0da 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -371,6 +371,12 @@ def get_creation_data( json = prompt_answer elif mnemonic: mnemonic = parse_mnemonic(mnemonic) + + if json: + if not os.path.exists(json): + print_error(f"The JSON file '{json}' does not exist.") + raise typer.Exit() + if json and not json_password: json_password = Prompt.ask( "Enter the backup password for JSON file.", password=True From e5546e9f46a18022f0dbe0f79f632e03d899a510 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 10:11:00 -0800 Subject: [PATCH 128/332] Dont show desc in-case of no stakes in btcli st list --- bittensor_cli/src/commands/stake/stake.py | 134 +++++++++++----------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 33198509..b819746f 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1720,74 +1720,76 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" ) + if not sub_stakes: + console.print(f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})") + else: + console.print("\nPress Enter to continue to column descriptions...") + input() + header = """ + [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) - console.print("\nPress Enter to continue to column descriptions...") - input() - header = """ -[bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: -""" - console.print(header) - description_table = Table( - show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True - ) - - fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), - ( - "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.", - ), - ( - "[bold tan]Stake (α)[/bold tan]", - "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Alpha Pool (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Registered[/bold tan]", - "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ] + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Stake (α)[/bold tan]", + "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Pool (τ_in)[/bold tan]", + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Alpha Pool (α_in)[/bold tan]", + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Alpha out (α_out)[/bold tan]", + "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Exchange Value (α x τ/α)[/bold tan]", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Swap (α → τ)[/bold tan]", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Registered[/bold tan]", + "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Emission (α/block)[/bold tan]", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ] - description_table.add_column( - "Field", - no_wrap=True, - style="bold tan", - ) - description_table.add_column("Description", overflow="fold") - for field_name, description in fields: - description_table.add_row(field_name, description) - console.print(description_table) + description_table.add_column( + "Field", + no_wrap=True, + style="bold tan", + ) + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def move_stake( From 7bb0f6081b4723eca32840748a00654219da5941 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 10:23:52 -0800 Subject: [PATCH 129/332] Adds validation for netuid options --- bittensor_cli/cli.py | 2 ++ bittensor_cli/src/bittensor/utils.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4f1ba0da..332d3603 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -43,6 +43,7 @@ print_error, validate_chain_endpoint, retry_prompt, + validate_netuid, ) from typing_extensions import Annotated from textwrap import dedent @@ -163,6 +164,7 @@ class Options: None, help="The netuid of the subnet in the root network, (e.g. 1).", prompt=True, + callback=validate_netuid, ) netuid_not_req = typer.Option( None, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index e65f665d..07a123d5 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -974,3 +974,9 @@ def retry_prompt( return var else: err_console.print(rejection_text) + + +def validate_netuid(value: int) -> int: + if value is not None and value < 0: + raise typer.BadParameter("Negative netuid passed. Please use correct netuid.") + return value From 9bcebb35ad28e35b03bb1d148804d4b2986fb27d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 10:43:27 -0800 Subject: [PATCH 130/332] Adds validation for amount in st add & remove --- bittensor_cli/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 332d3603..d2393f54 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2620,6 +2620,10 @@ def stake_add( amount = FloatPrompt.ask( "[dark_sea_green]Amount to stake to each netuid (TAO τ)[/dark_sea_green]" ) + + if amount <= 0: + print_error(f"You entered an incorrect stake amount: {amount}") + raise typer.Exit() if Balance.from_tao(amount) > free_balance: print_error( f"You dont have enough balance to stake. Current free Balance: {free_balance}." @@ -2796,6 +2800,10 @@ def stake_remove( else: excluded_hotkeys = [] + if amount and amount <= 0: + print_error(f"You entered an incorrect unstake amount: {amount}") + raise typer.Exit() + return self._run_command( stake.unstake( wallet, From c868c1467588ae5f08432981155f5a3fca2794bd Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 15:53:52 -0800 Subject: [PATCH 131/332] Renamed fields in stake_list --- bittensor_cli/src/commands/stake/stake.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b819746f..2f0894d1 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1596,8 +1596,8 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): str(netuid), # Number symbol, # Symbol f"{substake_.stake.tao:,.4f} {symbol}", # Stake (a) - f"{pool.tao_in.tao:,.4f} τ", # TAO pool (t_in) - f"{pool.alpha_in.tao:,.4f} {symbol}", # Alpha Pool a_in + f"{pool.tao_in.tao:,.4f} τ", # TAO Reserves (t_in) + f"{pool.alpha_in.tao:,.4f} {symbol}", # Alpha Reserves a_in f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) f"{pool.alpha_out.tao:,.4f} {symbol}", # Alpha out (a_out) f"[medium_purple]{tao_ownership}[/medium_purple]", # TAO equiv @@ -1638,12 +1638,12 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): justify="center", ) table.add_column( - f"[white]TAO pool ({Balance.unit}_in)", + f"[white]TAO Reserves ({Balance.unit}_in)", style="medium_purple", justify="right", ) table.add_column( - f"[white]Alpha pool ({Balance.get_unit(1)}_in)", + f"[white]Alpha Reserves ({Balance.get_unit(1)}_in)", style="medium_purple", justify="right", ) @@ -1698,7 +1698,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): hotkeys_to_substakes[hotkey].append(substake) # Iterate over each hotkey and make a table - counter = 0 + counter = 0 num_hotkeys = len(hotkeys_to_substakes) all_hotkeys_total_global_tao = Balance(0) all_hotkeys_total_tao_value = Balance(0) @@ -1744,16 +1744,16 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( - "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "ALPHA Pool (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Units of TAO in the TAO Reserves reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Reserves (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( - "[bold tan]Alpha Pool (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Pool(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Reserves(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool(τ_in) / ALPHA Pool (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Reserves(τ_in) / Alpha Reserves (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", ), ( "[bold tan]Alpha out (α_out)[/bold tan]", @@ -1761,7 +1761,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Pool(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', ), ( "[bold tan]Exchange Value (α x τ/α)[/bold tan]", From 27bea7a2d9728f13053e651cd98157356e8349df Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 4 Nov 2024 16:46:04 -0800 Subject: [PATCH 132/332] Integrates color palette to stake app --- bittensor_cli/cli.py | 11 +- bittensor_cli/src/__init__.py | 50 ++++++++ bittensor_cli/src/commands/stake/stake.py | 146 +++++++++++----------- bittensor_cli/src/commands/wallets.py | 12 +- 4 files changed, 135 insertions(+), 84 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d2393f54..0361cc6b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -24,6 +24,7 @@ WalletOptions as WO, WalletValidationTypes as WV, Constants, + COLOR_PALETTE, ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance @@ -298,7 +299,7 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - "[dark_sea_green3]Enter the netuid to use.[/dark_sea_green3] Leave blank for all netuids", + f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", default=None, show_default=False, ) @@ -823,7 +824,7 @@ def initialize_chain( elif self.config["network"]: self.subtensor = SubtensorInterface(self.config["network"]) console.print( - f"Using the specified network [dark_orange]{self.config['network']}[/dark_orange] from config" + f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config" ) else: self.subtensor = SubtensorInterface(defaults.subtensor.network) @@ -1185,7 +1186,7 @@ def wallet_ask( else: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue]" - + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-name`)[/dark_sea_green3 italic]", + + f" [{COLOR_PALETTE['GENERAL']['HINT']} italic](Hint: You can set this with `btcli config set --wallet-name`)", default=defaults.wallet.name, ) @@ -2614,11 +2615,11 @@ def stake_add( raise typer.Exit() if netuid is not None: amount = FloatPrompt.ask( - "[dark_sea_green]Amount to stake (TAO τ)[/dark_sea_green]" + f"Amount to [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]stake (TAO τ)" ) else: amount = FloatPrompt.ask( - "[dark_sea_green]Amount to stake to each netuid (TAO τ)[/dark_sea_green]" + f"Amount to [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]stake to each netuid (TAO τ)" ) if amount <= 0: diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 9e46757b..6cd3416b 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -550,3 +550,53 @@ class WalletValidationTypes(Enum): }, "WEIGHTS": {"COMMIT_REVEAL": "Commit / Reveal"}, } + +COLOR_PALETTE = { + "GENERAL": { + "HEADER": "#4196D6", + "LINKS": "#8CB9E9", + "HINT": "#A2E5B8", + "COLDKEY": "#9EF5E4", + "HOTKEY": "#ECC39D", + "SUBHEADING_MAIN": "#7ECFEC", + "SUBHEADING": "#AFEFFF", + "SUBHEADING_EXTRA_1": "#96A3C5", + "SUBHEADING_EXTRA_2": "#6D7BAF", + "CONFIRMATION_Y_N_Q": "#EE8DF8", + "SYMBOL": "#E7CC51", + "BALANCE": "#4F91C6" + }, + "STAKE": { + "STAKE_AMOUNT": "#53B5A0", + "STAKE_ALPHA": "#53B5A0", + "STAKE_SWAP": "#67A3A5", + "TAO": "#4F91C6", + "SLIPPAGE_TEXT": "#C25E7C", + "SLIPPAGE_PERCENT": "#E7B195", + "NOT_REGISTERED": "#EB6A6C", + "EXTRA_1": "#D781BB" + }, + "POOLS": { + "TAO": "#4F91C6", + "ALPHA_IN": "#D09FE9", + "ALPHA_OUT": "#AB7CC8", + "RATE": "#F8D384", + "TAO_EQUIV": "#8CB9E9", + "EMISSION": "#F8D384", + "EXTRA_1": "#CAA8FB", + "EXTRA_2": "#806DAF", + }, + "GREY": { + "GREY_100": "#F8F9FA", + "GREY_200": "#F1F3F4", + "GREY_300": "#DBDDE1", + "GREY_400": "#BDC1C6", + "GREY_500": "#5F6368", + "GREY_600": "#2E3134", + "GREY_700": "#282A2D", + "GREY_800": "#17181B", + "GREY_900": "#0E1013", + "BLACK": "#000000" + } + +} \ No newline at end of file diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2f0894d1..c51eb847 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -11,6 +11,7 @@ from rich import box from substrateinterface.exceptions import SubstrateRequestException +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( @@ -864,7 +865,7 @@ async def stake_add( ) # Init the table. table = Table( - title=f"\n[dark_orange]Staking to: \nWallet: [light_goldenrod2]{wallet.name}[/light_goldenrod2], Coldkey ss58: [light_goldenrod2]{wallet.coldkeypub.ss58_address}[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1002,7 +1003,7 @@ async def stake_add( slippage_pct = f"{slippage_pct_float:.4f} %" else: slippage_pct_float = 0 - slippage_pct = "N/A" + slippage_pct = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" max_slippage = max(slippage_pct_float, max_slippage) rows.append( ( @@ -1017,28 +1018,28 @@ async def stake_add( ) ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style="bright_magenta") + table.add_column("Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column( - f"Amount ({Balance.get_unit(0)})", justify="center", style="dark_sea_green" + f"Amount ({Balance.get_unit(0)})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO"] ) table.add_column( f"Rate (per {Balance.get_unit(0)})", justify="center", - style="light_goldenrod2", + style=COLOR_PALETTE["POOLS"]["RATE"], ) table.add_column( "Received", justify="center", - style="light_slate_blue", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - table.add_column("Slippage", justify="center", style="rgb(220,50,47)") + table.add_column("Slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) for row in rows: table.add_row(*row) console.print(table) message = "" if max_slippage > 5: - message += "-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold][yellow]WARNING:[/yellow] The slippage on one of your operations is high: [bold red]{max_slippage} %[/bold red], this may result in a loss of funds.[/bold] \n" + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" message += "-------------------------------------------------------------------------------------------------------------------\n" console.print(message) console.print( @@ -1056,7 +1057,7 @@ async def stake_add( ) if prompt: if not Confirm.ask("Would you like to continue?"): - return False + raise typer.Exit() async def send_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None @@ -1088,7 +1089,7 @@ async def send_extrinsic( return if not prompt: # TODO verbose? console.print( - f":white_heavy_check_mark: [green]Submitted {amount_} to {netuid_i}[/green]" + f":white_heavy_check_mark: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Submitted {amount_} to {netuid_i}[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) else: await response.process_events() @@ -1108,10 +1109,10 @@ async def send_extrinsic( new_balance = new_balance_[wallet.coldkeypub.ss58_address] new_stake = new_stake_.set_unit(netuid_i) console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( - f"Subnet: [dark_orange]{netuid_i}[/dark_orange] Stake:\n [blue]{current}[/blue] :arrow_right: [green]{new_stake}[/green]" + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) # Perform staking operation. @@ -1273,8 +1274,8 @@ async def unstake( # Prompt the user for each subnet while True: response = Prompt.ask( - f"Unstake all: [dark_sea_green3]{current_stake_balance}[/dark_sea_green3]" - f" from [dark_sea_green3]{staking_address_name if staking_address_name else staking_address_ss58}[/dark_sea_green3] on netuid: [dark_sea_green3]{netuid}? [y/n/q]", + f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]", choices=["y", "n", "q"], default="n", show_choices=True, @@ -1290,7 +1291,7 @@ async def unstake( elif response.lower() == "n": while True: amount_input = Prompt.ask( - f"Enter amount to unstake in [dark_sea_green3]{Balance.get_unit(netuid)}[/dark_sea_green3] from subnet: [dark_sea_green3]{netuid}[/dark_sea_green3] (Max: [dark_sea_green3]{current_stake_balance}[/dark_sea_green3])" + f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])" ) if amount_input.lower() == "q": skip_remaining_subnets = True @@ -1382,7 +1383,7 @@ async def unstake( # Build the table table = Table( - title=f"\n[tan]Unstaking to: \nWallet: [dark_sea_green3]{wallet.name}[/dark_sea_green3], Coldkey ss58: [dark_sea_green3]{wallet.coldkeypub.ss58_address}[/dark_sea_green3]\nNetwork: {subtensor.network}[/tan]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1393,22 +1394,22 @@ async def unstake( pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style="plum2") + table.add_column("Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column( - f"Amount ({Balance.get_unit(1)})", justify="center", style="dark_sea_green" + f"Amount ({Balance.get_unit(1)})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO"] ) table.add_column( f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", justify="center", - style="light_goldenrod2", + style=COLOR_PALETTE["POOLS"]["RATE"], ) table.add_column( f"Received ({Balance.get_unit(0)})", justify="center", - style="light_slate_blue", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], footer=f"{total_received_amount}", ) - table.add_column("Slippage", justify="center", style="light_salmon3") + table.add_column("Slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) for op in unstake_operations: dynamic_info = op["dynamic_info"] @@ -1427,8 +1428,8 @@ async def unstake( if max_float_slippage > 5: console.print( "\n" - f"-------------------------------------------------------------------------------------------------------------------\n" - f"[bold][yellow]WARNING:[/yellow] The slippage on one of your operations is high: [bold red]{max_float_slippage}%[/bold red], this may result in a loss of funds.[/bold] \n" + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" f"-------------------------------------------------------------------------------------------------------------------\n" ) @@ -1447,7 +1448,7 @@ async def unstake( ) if prompt: if not Confirm.ask("Would you like to continue?"): - return False + raise typer.Exit() # Perform unstaking operations try: @@ -1506,12 +1507,12 @@ async def unstake( ) ).set_unit(netuid_i) console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( - f"Subnet: [dark_orange]{netuid_i}[/dark_orange] Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [green]{new_stake}[/green]" + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - console.print("[green]Unstaking operations completed.[/green]") + console.print(f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed.") async def stake_list( @@ -1572,9 +1573,9 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): if slippage + swapped_tao_value != 0 else 0 ) - slippage_percentage = f"[salmon1]{slippage_percentage_:.3f}%[/salmon1]" + slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" else: - slippage_percentage = "[salmon1]0.000%[/salmon1]" + slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" tao_locked = pool.tao_in issuance = pool.alpha_out if pool.is_dynamic else tao_locked per_block_emission = substake_.emission.tao / emission_drain_tempo @@ -1600,20 +1601,20 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): f"{pool.alpha_in.tao:,.4f} {symbol}", # Alpha Reserves a_in f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) f"{pool.alpha_out.tao:,.4f} {symbol}", # Alpha out (a_out) - f"[medium_purple]{tao_ownership}[/medium_purple]", # TAO equiv - f"[light_slate_blue]{tao_value}[/light_slate_blue]", # Exchange Value (α x τ/α) - f"[cadet_blue]{swapped_tao_value}[/cadet_blue] ({slippage_percentage})", # Swap(α) -> τ - "[bold cadet_blue]YES[/bold cadet_blue]" + f"{tao_ownership}", # TAO equiv + f"{tao_value}", # Exchange Value (α x τ/α) + f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ + "YES" if substake_.is_registered - else "[hot_pink3]NO[/hot_pink3]", # Registered + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered str(Balance.from_tao(per_block_emission).set_unit(netuid)) if substake_.is_registered - else "[hot_pink3]N/A[/hot_pink3]", # Emission(α/block) + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) ] ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") table = Table( - title=f"\n[dark_orange]Hotkey: {name}[/dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n\nSee below for an explanation of the columns\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {name}\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n\n[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1626,62 +1627,61 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): table.add_column("[white]Netuid", footer_style="overline white", style="grey89") table.add_column( "[white]Symbol", - footer_style="white", - style="light_goldenrod1", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="center", no_wrap=True, ) table.add_column( f"[white]Stake ({Balance.get_unit(1)})", footer_style="overline white", - style="rgb(42,161,152)", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="center", ) table.add_column( f"[white]TAO Reserves ({Balance.unit}_in)", - style="medium_purple", + style=COLOR_PALETTE["STAKE"]["TAO"], justify="right", ) table.add_column( f"[white]Alpha Reserves ({Balance.get_unit(1)}_in)", - style="medium_purple", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="right", ) table.add_column( f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", footer_style="white", - style="light_goldenrod2", + style=COLOR_PALETTE["POOLS"]["RATE"], justify="center", ) table.add_column( f"[white]Alpha out ({Balance.get_unit(1)}_out)", - style="medium_purple", + style=COLOR_PALETTE["POOLS"]["ALPHA_OUT"], justify="right", ) table.add_column( f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", - style="medium_purple", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], justify="right", footer=f"{total_tao_ownership}", ) table.add_column( f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", footer_style="overline white", - style="blue", + style=COLOR_PALETTE["STAKE"]["TAO"], justify="right", footer=f"{total_tao_value}", ) table.add_column( f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", footer_style="overline white", - style="white", + style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], justify="right", footer=f"{total_swapped_tao_value}", ) - table.add_column("[white]Registered", style="red", justify="right") + table.add_column("[white]Registered",style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="right") table.add_column( f"[white]Emission \n({Balance.get_unit(1)}/block)", - style="light_goldenrod2", + style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="right", ) for row in rows: @@ -1715,10 +1715,10 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): console.print("\n\n") console.print( f"Wallet:\n" - f" Coldkey SS58: [bold plum2]{coldkey_address}[/bold plum2]\n" - f" Free Balance: [dark_sea_green]{balance}[/dark_sea_green]\n" - f" Total TAO ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_global_tao}[/dark_sea_green]\n" - f" Total Value ({Balance.unit}): [dark_sea_green]{all_hotkeys_total_tao_value}[/dark_sea_green]" + f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_global_tao}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) if not sub_stakes: console.print(f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})") @@ -1822,15 +1822,15 @@ async def move_stake( if origin_stake_balance == Balance.from_tao(0).set_unit(origin_netuid): print_error( - f"Your balance is [dark_orange]0[/dark_orange] in Netuid: [dark_orange]{origin_netuid}[/dark_orange]" + f"Your balance is [{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] in Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}" ) raise typer.Exit() console.print( - f"\nOrigin netuid: [dark_orange]{origin_netuid}[/dark_orange], Origin stake: [dark_orange]{origin_stake_balance}" + f"\nOrigin Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Origin stake: [{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}" ) console.print( - f"Destination netuid: [dark_orange]{destination_netuid}[/dark_orange], Destination stake: [dark_orange]{destination_stake_balance}\n" + f"Destination netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Destination stake: [{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}\n" ) # Determine the amount we are moving. @@ -1841,13 +1841,13 @@ async def move_stake( amount_to_move_as_balance = origin_stake_balance else: # max_stake # TODO improve this - if Confirm.ask(f"Move all: [dark_orange]{origin_stake_balance}[/dark_orange]?"): + if Confirm.ask(f"Move all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]?"): amount_to_move_as_balance = origin_stake_balance else: try: amount = float( Prompt.ask( - f"Enter amount to move in [dark_orange]{Balance.get_unit(origin_netuid)}" + f"Enter amount to move in [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}" ) ) amount_to_move_as_balance = Balance.from_tao(amount) @@ -1859,7 +1859,7 @@ async def move_stake( amount_to_move_as_balance.set_unit(origin_netuid) if amount_to_move_as_balance > origin_stake_balance: err_console.print( - f"[red]Not enough stake[/red]:\n Stake balance:[dark_orange]{origin_stake_balance}[/dark_orange] < Moving amount: [dark_orange]{amount_to_move_as_balance}[/dark_orange]" + f"[red]Not enough stake[/red]:\n Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) return False @@ -1903,7 +1903,7 @@ async def move_stake( ) table = Table( - title=f"\n[dark_orange]Moving stake from: [light_goldenrod2]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/light_goldenrod2] to: [light_goldenrod2]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/light_goldenrod2]\nNetwork: {subtensor.network}[/dark_orange]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Moving stake from: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1913,26 +1913,26 @@ async def move_stake( show_lines=False, pad_edge=True, ) - table.add_column("origin netuid", justify="center", style="green") - table.add_column("origin hotkey", justify="center", style="bright_magenta") - table.add_column("dest netuid", justify="center", style="green") - table.add_column("dest hotkey", justify="center", style="bright_magenta") + table.add_column("origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) + table.add_column("origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column("dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) + table.add_column("dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column( f"amount ({Balance.get_unit(origin_netuid)})", justify="center", - style="medium_purple", + style=COLOR_PALETTE["STAKE"]["TAO"], ) table.add_column( f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", justify="center", - style="cyan", + style=COLOR_PALETTE["POOLS"]["RATE"], ) table.add_column( f"received ({Balance.get_unit(destination_netuid)})", justify="center", - style="rgb(220,50,47)", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - table.add_column("slippage", justify="center", style="salmon1") + table.add_column("slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) table.add_row( f"{Balance.get_unit(origin_netuid)}({origin_netuid})", @@ -1949,9 +1949,9 @@ async def move_stake( console.print(table) message = "" if slippage_pct_float > 5: - message += "\t-------------------------------------------------------------------------------------------------------------------\n" - message += f"\t[bold][yellow]WARNING:[/yellow]\tSlippage is high: [bold red]{slippage_pct}[/bold red], this may result in a loss of funds.[/bold] \n" - message += "\t-------------------------------------------------------------------------------------------------------------------\n" + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n" + message += "-------------------------------------------------------------------------------------------------------------------\n" console.print(message) if not Confirm.ask("Would you like to continue?"): return True @@ -2011,10 +2011,10 @@ async def move_stake( ).set_unit(destination_netuid) console.print( f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " - f"[green]{new_origin_stake_balance}[/green]" + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" ) console.print( f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " - f"[green]{new_destination_stake_balance}[/green]" + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) return diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ce504118..0c4d0d4d 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -25,7 +25,7 @@ import scalecodec import typer -from bittensor_cli.src import TYPE_REGISTRY +from bittensor_cli.src import TYPE_REGISTRY, COLOR_PALETTE from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -269,28 +269,28 @@ async def wallet_balance( ), Column( "[white]Coldkey Address", - style="plum2", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], no_wrap=True, ), Column( "[white]Free Balance", justify="right", - style="tan", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), Column( "[white]Staked Balance", justify="right", - style="orange1", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], no_wrap=True, ), Column( "[white]Total Balance", justify="right", - style="dark_sea_green", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), - title=f"\n[underline navajo_white1]Wallet Coldkey Balance[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}", + title=f"\n [{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance\nNetwork: {subtensor.network}", show_footer=True, show_edge=False, border_style="bright_black", From df654ad1aaae54e46dc71848287383a675c2ac97 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 5 Nov 2024 14:45:50 -0800 Subject: [PATCH 133/332] Disables overview, inspect, history, metagraph for rao games --- bittensor_cli/cli.py | 49 +++++++++++++++++++++++----- bittensor_cli/src/bittensor/utils.py | 34 +++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0361cc6b..33ad573c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -45,6 +45,8 @@ validate_chain_endpoint, retry_prompt, validate_netuid, + is_rao_network, + get_effective_network, ) from typing_extensions import Annotated from textwrap import dedent @@ -735,9 +737,9 @@ def __init__(self): self.subnets_app.command( "register", rich_help_panel=HELP_PANELS["SUBNETS"]["REGISTER"] )(self.subnets_register) - # self.subnets_app.command( - # "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] - # )(self.subnets_metagraph) + self.subnets_app.command( + "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True + )(self.subnets_metagraph) self.subnets_app.command( "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) @@ -1396,6 +1398,12 @@ def wallet_overview( "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + return self._run_command( wallets.overview( wallet, @@ -1464,6 +1472,13 @@ def wallet_transfer( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + subtensor = self.initialize_chain(network) return self._run_command( wallets.transfer( @@ -1589,6 +1604,12 @@ def wallet_inspect( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate ) + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + self.initialize_chain(network) return self._run_command( wallets.inspect( @@ -2138,13 +2159,17 @@ def wallet_history( [green]$[/green] btcli wallet history """ + # TODO: Fetch effective network and redirect users accordingly - this only works on finney + # no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" - no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" - - if self.config.get("network"): - if self.config.get("network") != "finney": - console.print(no_use_config_str) - + # if self.config.get("network"): + # if self.config.get("network") != "finney": + # console.print(no_use_config_str) + + # For Rao games + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -3726,6 +3751,12 @@ def subnets_metagraph( ) raise typer.Exit() + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + if reuse_last: if netuid is not None: console.print("Cannot specify netuid when using `--reuse-last`") diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 07a123d5..ffe319d1 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -23,6 +23,7 @@ from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src import defaults, Constants if TYPE_CHECKING: @@ -980,3 +981,36 @@ def validate_netuid(value: int) -> int: if value is not None and value < 0: raise typer.BadParameter("Negative netuid passed. Please use correct netuid.") return value + + +def get_effective_network(config, network: Optional[list[str]]) -> str: + """ + Determines the effective network to be used, considering the network parameter, + the configuration, and the default. + """ + if network: + for item in network: + if item.startswith("ws"): + network_ = item + break + else: + network_ = item + return network_ + elif config.get("network"): + return config["network"] + else: + return defaults.subtensor.network + +def is_rao_network(network: str) -> bool: + """Check if the given network is 'rao'.""" + + network = network.lower() + rao_identifiers = [ + "rao", + Constants.rao_entrypoint, + ] + return ( + network == "rao" + or network in rao_identifiers + or "rao.chain.opentensor.ai" in network + ) From 4b84b86f7265fc81d67f481f2e8ae12deb8edbe2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 5 Nov 2024 15:12:42 -0800 Subject: [PATCH 134/332] Adds option to skip printing info tables in s list, st list --- bittensor_cli/src/commands/stake/stake.py | 198 +++++++++++++--------- bittensor_cli/src/commands/subnets.py | 110 ++++++------ 2 files changed, 179 insertions(+), 129 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index c51eb847..5507cec4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1018,9 +1018,13 @@ async def stake_add( ) ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column( - f"Amount ({Balance.get_unit(0)})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO"] + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], ) table.add_column( f"Rate (per {Balance.get_unit(0)})", @@ -1032,7 +1036,9 @@ async def stake_add( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - table.add_column("Slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) for row in rows: table.add_row(*row) console.print(table) @@ -1394,9 +1400,13 @@ async def unstake( pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") - table.add_column("Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column( - f"Amount ({Balance.get_unit(1)})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO"] + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], ) table.add_column( f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", @@ -1409,7 +1419,9 @@ async def unstake( style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], footer=f"{total_received_amount}", ) - table.add_column("Slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) for op in unstake_operations: dynamic_info = op["dynamic_info"] @@ -1512,7 +1524,9 @@ async def unstake( console.print( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - console.print(f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed.") + console.print( + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." + ) async def stake_list( @@ -1678,7 +1692,11 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): justify="right", footer=f"{total_swapped_tao_value}", ) - table.add_column("[white]Registered",style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="right") + table.add_column( + "[white]Registered", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="right", + ) table.add_column( f"[white]Emission \n({Balance.get_unit(1)}/block)", style=COLOR_PALETTE["POOLS"]["EMISSION"], @@ -1723,73 +1741,81 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): if not sub_stakes: console.print(f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})") else: - console.print("\nPress Enter to continue to column descriptions...") - input() - header = """ - [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: - """ - console.print(header) - description_table = Table( - show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True - ) + display_table = Prompt.ask( + "\nPress Enter to view column descriptions or type 'q' to skip:", + choices=["", "q"], + default="", + show_choices=True, + ).lower() + + if display_table == "q": + console.print(f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped.") + else: + header = """ + [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) - fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), - ( - "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.", - ), - ( - "[bold tan]Stake (α)[/bold tan]", - "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Reserves (τ_in)[/bold tan]", - 'Units of TAO in the TAO Reserves reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Reserves (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Alpha Reserves (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Reserves(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Reserves(τ_in) / Alpha Reserves (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Registered[/bold tan]", - "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ] + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Stake (α)[/bold tan]", + "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Units of TAO in the TAO Reserves reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Reserves (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Reserves(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Reserves(τ_in) / Alpha Reserves (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Alpha out (α_out)[/bold tan]", + "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Exchange Value (α x τ/α)[/bold tan]", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Swap (α → τ)[/bold tan]", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Registered[/bold tan]", + "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Emission (α/block)[/bold tan]", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ] - description_table.add_column( - "Field", - no_wrap=True, - style="bold tan", - ) - description_table.add_column("Description", overflow="fold") - for field_name, description in fields: - description_table.add_row(field_name, description) - console.print(description_table) + description_table.add_column( + "Field", + no_wrap=True, + style="bold tan", + ) + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def move_stake( @@ -1841,7 +1867,9 @@ async def move_stake( amount_to_move_as_balance = origin_stake_balance else: # max_stake # TODO improve this - if Confirm.ask(f"Move all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]?"): + if Confirm.ask( + f"Move all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]?" + ): amount_to_move_as_balance = origin_stake_balance else: try: @@ -1913,10 +1941,18 @@ async def move_stake( show_lines=False, pad_edge=True, ) - table.add_column("origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) - table.add_column("origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) - table.add_column("dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) - table.add_column("dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column( + "origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + "dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) table.add_column( f"amount ({Balance.get_unit(origin_netuid)})", justify="center", @@ -1932,7 +1968,11 @@ async def move_stake( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - table.add_column("slippage", justify="center", style=COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']) + table.add_column( + "slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) table.add_row( f"{Balance.get_unit(origin_netuid)}({origin_netuid})", diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 48b07e0c..476af591 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -5,7 +5,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm +from rich.prompt import Confirm, Prompt from rich.console import Console, Group from rich.spinner import Spinner from rich.text import Text @@ -13,6 +13,7 @@ from rich.table import Column, Table from rich import box +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( @@ -482,57 +483,66 @@ def format_cell(value, previous_value, unit="", precision=4): table = create_table(subnets, global_weights) console.print(table) - console.print("\nPress Enter to continue to column descriptions...") - input() - header = """ -[bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: -""" - console.print(header) - description_table = Table( - show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True - ) + display_table = Prompt.ask( + "\nPress Enter to view column descriptions or type 'q' to skip:", + choices=["", "q"], + default="", + ).lower() - fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), - ( - "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.", - ), - ( - "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Alpha Pool (α_in)[/bold tan]", - "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Tempo (k/n)[/bold tan]", - 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', - ), - ( - "[bold tan]Local weight coeff (γ)[/bold tan]", - "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ] + if display_table == "q": + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + ) + else: + header = """ + [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Emission (τ)[/bold tan]", + "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]STAKE (α_out)[/bold tan]", + "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]TAO Pool (τ_in)[/bold tan]", + 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Alpha Pool (α_in)[/bold tan]", + "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Tempo (k/n)[/bold tan]", + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + ), + ( + "[bold tan]Local weight coeff (γ)[/bold tan]", + "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ] - description_table.add_column("Field", no_wrap=True, style="bold tan") - description_table.add_column("Description", overflow="fold") - for field_name, description in fields: - description_table.add_row(field_name, description) - console.print(description_table) + description_table.add_column("Field", no_wrap=True, style="bold tan") + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def show(subtensor: "SubtensorInterface", netuid: int, prompt: bool = True): From c7677412d0c4d5ad6b67b7952639275630cd3aac Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 5 Nov 2024 16:37:32 -0800 Subject: [PATCH 135/332] Integrates palette to stake app --- bittensor_cli/src/__init__.py | 21 ++-- .../src/bittensor/extrinsics/registration.py | 27 ++--- bittensor_cli/src/commands/stake/stake.py | 4 +- bittensor_cli/src/commands/subnets.py | 98 +++++++++++++------ bittensor_cli/src/commands/sudo.py | 13 +-- 5 files changed, 105 insertions(+), 58 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6cd3416b..b3077d8e 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -564,7 +564,12 @@ class WalletValidationTypes(Enum): "SUBHEADING_EXTRA_2": "#6D7BAF", "CONFIRMATION_Y_N_Q": "#EE8DF8", "SYMBOL": "#E7CC51", - "BALANCE": "#4F91C6" + "BALANCE": "#4F91C6", + "COST": "#53B5A0", + "SUCCESS": "#53B5A0", + "NETUID": "#CBA880", + "NETUID_EXTRA": "#DDD5A9", + "TEMPO": "#67A3A5", }, "STAKE": { "STAKE_AMOUNT": "#53B5A0", @@ -574,7 +579,7 @@ class WalletValidationTypes(Enum): "SLIPPAGE_TEXT": "#C25E7C", "SLIPPAGE_PERCENT": "#E7B195", "NOT_REGISTERED": "#EB6A6C", - "EXTRA_1": "#D781BB" + "EXTRA_1": "#D781BB", }, "POOLS": { "TAO": "#4F91C6", @@ -596,7 +601,11 @@ class WalletValidationTypes(Enum): "GREY_700": "#282A2D", "GREY_800": "#17181B", "GREY_900": "#0E1013", - "BLACK": "#000000" - } - -} \ No newline at end of file + "BLACK": "#000000", + }, + "SUDO": { + "HYPERPARAMETER": "#4F91C6", + "VALUE": "#D09FE9", + "NORMALIZED": "#AB7CC8", + }, +} diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index b45b3a5a..ae5db605 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -28,6 +28,7 @@ from rich.status import Status from substrateinterface.exceptions import SubstrateRequestException +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import NeuronInfo from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( @@ -532,9 +533,9 @@ async def get_neuron_for_pubkey_and_subnet(): if prompt: if not Confirm.ask( f"Continue Registration?\n" - f" hotkey ({wallet.hotkey_str}):\t[bold white]{wallet.hotkey.ss58_address}[/bold white]\n" - f" coldkey ({wallet.name}):\t[bold white]{wallet.coldkeypub.ss58_address}[/bold white]\n" - f" network:\t\t[bold white]{subtensor.network}[/bold white]" + f" hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]({wallet.hotkey_str})[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]:\t[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" + f" coldkey [{COLOR_PALETTE['GENERAL']['COLDKEY']}]({wallet.name})[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]:\t[{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f" network:\t\t[{COLOR_PALETTE['GENERAL']['LINKS']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['LINKS']}]\n" ): return False @@ -587,7 +588,7 @@ async def get_neuron_for_pubkey_and_subnet(): ) if is_registered: err_console.print( - f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]" + f":white_heavy_check_mark: [dark_sea_green3]Already registered on netuid:{netuid}[/dark_sea_green3]" ) return True @@ -634,8 +635,8 @@ async def get_neuron_for_pubkey_and_subnet(): if "HotKeyAlreadyRegisteredInSubNet" in err_msg: console.print( - f":white_heavy_check_mark: [green]Already Registered on " - f"[bold]subnet:{netuid}[/bold][/green]" + f":white_heavy_check_mark: [dark_sea_green3]Already Registered on " + f"[bold]subnet:{netuid}[/bold][/dark_sea_green3]" ) return True err_console.print( @@ -653,7 +654,7 @@ async def get_neuron_for_pubkey_and_subnet(): ) if is_registered: console.print( - ":white_heavy_check_mark: [green]Registered[/green]" + ":white_heavy_check_mark: [dark_sea_green3]Registered[/dark_sea_green3]" ) return True else: @@ -728,11 +729,11 @@ async def burned_register_extrinsic( if not neuron.is_null: console.print( - ":white_heavy_check_mark: [green]Already Registered[/green]:\n" - f"uid: [bold white]{neuron.uid}[/bold white]\n" - f"netuid: [bold white]{neuron.netuid}[/bold white]\n" - f"hotkey: [bold white]{neuron.hotkey}[/bold white]\n" - f"coldkey: [bold white]{neuron.coldkey}[/bold white]" + ":white_heavy_check_mark: [dark_sea_green3]Already Registered[/dark_sea_green3]:\n" + f"uid: [{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]{neuron.uid}[/{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]\n" + f"netuid: [{COLOR_PALETTE['GENERAL']['NETUID']}]{neuron.netuid}[/{COLOR_PALETTE['GENERAL']['NETUID']}]\n" + f"hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{neuron.hotkey}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" + f"coldkey: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{neuron.coldkey}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" ) return True @@ -775,7 +776,7 @@ async def burned_register_extrinsic( console.print( "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" + f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance[wallet.coldkey.ss58_address]}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) if len(netuids_for_hotkey) > 0: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 5507cec4..aefa7f52 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1749,7 +1749,9 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ).lower() if display_table == "q": - console.print(f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped.") + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + ) else: header = """ [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 476af591..b9ad3177 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -95,15 +95,17 @@ async def _find_event_attributes_in_extrinsic_receipt( burn_cost = await lock_cost(subtensor) if burn_cost > your_balance: err_console.print( - f"Your balance of: [green]{your_balance}[/green] is not enough to pay the subnet lock cost of: " - f"[green]{burn_cost}[/green]" + f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}] is not enough to pay the subnet lock cost of: " + f"[{COLOR_PALETTE['POOLS']['TAO']}]{burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}]" ) return False if prompt: - console.print(f"Your balance is: [green]{your_balance}[/green]") + console.print( + f"Your balance is: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}" + ) if not Confirm.ask( - f"Do you want to register a subnet for [green]{burn_cost}[/green]?" + f"Do you want to register a subnet for [{COLOR_PALETTE['POOLS']['TAO']}]{burn_cost}?" ): return False @@ -151,7 +153,7 @@ async def _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) console.print( - f":white_heavy_check_mark: [green]Registered subnetwork with netuid: {attributes[0]}[/green]" + f":white_heavy_check_mark: [{COLOR_PALETTE['GENERAL']['SUCCESS']}]Registered subnetwork with netuid: {attributes[0]}" ) return True @@ -177,8 +179,8 @@ async def fetch_subnet_data(): def define_table(total_emissions: float, total_rate: float): table = Table( - title=f"\n[underline navajo_white1]Subnets[/underline navajo_white1]" - f"\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", show_footer=True, show_edge=False, header_style="bold white", @@ -189,38 +191,42 @@ def define_table(total_emissions: float, total_rate: float): pad_edge=True, ) - table.add_column("[bold white]NETUID", style="white", justify="center") - table.add_column("[bold white]SYMBOL", style="bright_cyan", justify="right") + table.add_column("[bold white]NETUID", style="grey89", justify="center") + table.add_column( + "[bold white]SYMBOL", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="right", + ) table.add_column( f"[bold white]EMISSION ({Balance.get_unit(0)})", - style="tan", + style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="left", footer=f"τ {total_emissions:.4f}", ) table.add_column( f"[bold white]STAKE ({Balance.get_unit(1)}_out)", - style="light_salmon3", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) table.add_column( f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", - style="rgb(42,161,152)", + style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", ) table.add_column( f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", - style="rgb(42,161,152)", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="left", ) table.add_column( f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", - style="medium_purple", + style=COLOR_PALETTE["POOLS"]["RATE"], justify="left", footer=f"τ {total_rate:.4f}", ) table.add_column( "[bold white]Tempo (k/n)", - style="plum2", + style=COLOR_PALETTE["GENERAL"]["TEMPO"], justify="left", overflow="fold", ) @@ -672,7 +678,8 @@ async def show_subnet(netuid_: int): # Define table properties table = Table( - title=f"[underline navajo_white1]Subnet {netuid_}[/underline navajo_white1]\n[navajo_white1]Network: {subtensor.network}[/navajo_white1]\n", + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -692,6 +699,7 @@ async def show_subnet(netuid_: int): tao_sum = Balance(0) stake_sum = Balance(0) relative_emissions_sum = 0 + stake_weight_sum = 0 for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum @@ -701,6 +709,7 @@ async def show_subnet(netuid_: int): relative_emissions_sum += hotkey_block_emission tao_sum += subnet_state.global_stake[idx] stake_sum += subnet_state.local_stake[idx] + stake_weight_sum += subnet_state.stake_weight[idx] rows.append( ( str(idx), # UID @@ -720,14 +729,14 @@ async def show_subnet(netuid_: int): table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( f"TAO({Balance.get_unit(0)})", - style="medium_purple", + style=COLOR_PALETTE["STAKE"]["TAO"], no_wrap=True, justify="right", footer=str(tao_sum), ) table.add_column( f"Stake({Balance.get_unit(netuid_)})", - style="rgb(42,161,152)", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", footer=f"{stake_sum.set_unit(subnet_info.netuid)}", @@ -737,10 +746,11 @@ async def show_subnet(netuid_: int): style="blue", no_wrap=True, justify="center", + footer=f"{stake_weight_sum:.3f}", ) table.add_column( "Dividends", - style="#8787d7", + style=COLOR_PALETTE["POOLS"]["EMISSION"], no_wrap=True, justify="center", footer=f"{relative_emissions_sum:.3f}", @@ -757,13 +767,23 @@ async def show_subnet(netuid_: int): # ) table.add_column( f"Emissions ({Balance.get_unit(netuid_)})", - style="tan", + style=COLOR_PALETTE["POOLS"]["EMISSION"], no_wrap=True, justify="center", footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), ) - table.add_column("Hotkey", style="plum2", no_wrap=True, justify="center") - table.add_column("Coldkey", style="plum2", no_wrap=True, justify="center") + table.add_column( + "Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + no_wrap=True, + justify="center", + ) + table.add_column( + "Coldkey", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + no_wrap=True, + justify="center", + ) for row in rows: table.add_row(*row) @@ -772,7 +792,9 @@ async def show_subnet(netuid_: int): console.print(table) console.print("\n") console.print( - f"Subnet: {netuid_}:\n Owner: [bold plum2]{subnet_info.owner}[/bold plum2]\n Total Locked: [dark_sea_green]{subnet_info.total_locked}[/dark_sea_green]\n Owner Locked: [dark_sea_green]{subnet_info.owner_locked}[/dark_sea_green]" + f"Subnet: {netuid_}:\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f" Total Locked: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{subnet_info.total_locked}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Owner Locked: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{subnet_info.owner_locked}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) console.print( """ @@ -810,7 +832,9 @@ async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: ) if lc: lock_cost_ = Balance(lc) - console.print(f"Subnet lock cost: [green]{lock_cost_}[/green]") + console.print( + f"Subnet lock cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{lock_cost_}" + ) return lock_cost_ else: err_console.print("Subnet lock cost: [red]Failed to get subnet lock cost[/red]") @@ -898,7 +922,8 @@ async def register( # TODO make this a reusable function, also used in subnets list # Show creation table. table = Table( - title=f"\n[white]Register to netuid [dark_orange]{netuid}[/dark_orange]\nNetwork: [dark_orange]{subtensor.network}[/dark_orange]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Register to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]netuid: {netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -912,23 +937,32 @@ async def register( "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" ) table.add_column( - "Symbol", style="rgb(211,54,130)", no_wrap=True, justify="center" + "Symbol", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + no_wrap=True, + justify="center", ) table.add_column( f"Cost ({Balance.get_unit(0)})", - style="tan", + style=COLOR_PALETTE["POOLS"]["TAO"], no_wrap=True, justify="center", ) table.add_column( - "Hotkey", style="bright_magenta", no_wrap=True, justify="center" + "Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + no_wrap=True, + justify="center", ) table.add_column( - "Coldkey", style="bold bright_magenta", no_wrap=True, justify="center" + "Coldkey", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + no_wrap=True, + justify="center", ) table.add_row( str(netuid), - f"[light_goldenrod1]{Balance.get_unit(netuid)}[light_goldenrod1]", + f"{Balance.get_unit(netuid)}", f"τ {current_recycle.tao:.4f}", f"{wallet.hotkey.ss58_address}", f"{wallet.coldkeypub.ss58_address}", @@ -936,8 +970,8 @@ async def register( console.print(table) if not ( Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is " - f"[bold red]{current_recycle}[/bold red]\nDo you want to continue?", + f"Your balance is: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\nThe cost to register by recycle is " + f"[{COLOR_PALETTE['GENERAL']['COST']}]{current_recycle}[/{COLOR_PALETTE['GENERAL']['COST']}]\nDo you want to continue?", default=False, ) ): diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 36bab3a1..83f0c9a6 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -8,7 +8,7 @@ from rich.prompt import Confirm from scalecodec import GenericCall -from bittensor_cli.src import HYPERPARAMS, DelegatesDetails +from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -496,11 +496,12 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): subnet = await subtensor.get_subnet_hyperparameters(netuid) table = Table( - Column("[white]HYPERPARAMETER", style="plum2"), - Column("[white]VALUE", style="light_goldenrod2"), - Column("[white]NORMALIZED", style="light_goldenrod3"), - title=f"[underline navajo_white1]\nSubnet Hyperparameters[/underline navajo_white1]\n NETUID: [navajo_white1]" - f"{netuid}[/navajo_white1] - Network: [navajo_white1]{subtensor.network}[/navajo_white1]\n", + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE['SUDO']['HYPERPARAMETER']), + Column("[white]VALUE", style=COLOR_PALETTE['SUDO']['VALUE']), + Column("[white]NORMALIZED", style=COLOR_PALETTE['SUDO']['NORMALIZED']), + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, width=None, pad_edge=False, From 966fa5298b9559a8e629cc266977bb1fd144cc61 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 5 Nov 2024 17:25:35 -0800 Subject: [PATCH 136/332] Partial: integrates new palette to sudo, wallet apps --- bittensor_cli/cli.py | 41 +++++++++++++-------------- bittensor_cli/src/commands/sudo.py | 16 +++++------ bittensor_cli/src/commands/wallets.py | 6 ++-- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 33ad573c..10b56eab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1183,7 +1183,7 @@ def wallet_ask( if self.config.get("wallet_name"): wallet_name = self.config.get("wallet_name") console.print( - f"Using the wallet name from config:[bold cyan] {wallet_name}" + f"Using the [blue]wallet name[/blue] from config:[bold cyan] {wallet_name}" ) else: wallet_name = Prompt.ask( @@ -1196,7 +1196,7 @@ def wallet_ask( if self.config.get("wallet_hotkey"): wallet_hotkey = self.config.get("wallet_hotkey") console.print( - f"Using the wallet hotkey from config:[bold cyan] {wallet_hotkey}" + f"Using the [blue]wallet hotkey[/blue] from config:[bold cyan] {wallet_hotkey}" ) else: wallet_hotkey = Prompt.ask( @@ -1211,7 +1211,7 @@ def wallet_ask( elif self.config.get("wallet_path"): wallet_path = self.config.get("wallet_path") console.print( - f"Using the wallet path from config:[bold magenta] {wallet_path}" + f"Using the [blue]wallet path[/blue] from config:[bold magenta] {wallet_path}" ) if WO.PATH in ask_for and not wallet_path: @@ -1751,7 +1751,7 @@ def wallet_regen_coldkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1805,7 +1805,7 @@ def wallet_regen_coldkey_pub( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1920,12 +1920,12 @@ def wallet_new_hotkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the wallet name", default=defaults.wallet.name + f"Enter the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]wallet name", default=defaults.wallet.name ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - "Enter the name of the new hotkey", default=defaults.wallet.hotkey + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", default=defaults.wallet.hotkey ) wallet = self.wallet_ask( @@ -1975,7 +1975,7 @@ def wallet_new_coldkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name ) wallet = self.wallet_ask( @@ -2043,12 +2043,12 @@ def wallet_create_wallet( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet (coldkey)", + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - "Enter the the name of the new hotkey", default=defaults.wallet.hotkey + f"Enter the the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", default=defaults.wallet.hotkey ) self.verbosity_handler(quiet, verbose) @@ -2426,8 +2426,9 @@ def wallet_sign( self.verbosity_handler(quiet, verbose) if use_hotkey is None: use_hotkey = Confirm.ask( - "Would you like to sign the transaction using your [red]hotkey[/red]?" - "\n[Type [red]y[/red] for [red]hotkey[/red] and [blue]n[/blue] for [blue]coldkey[/blue]] (default is [blue]coldkey[/blue])", + f"Would you like to sign the transaction using your [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]?" + f"\n[Type [{COLOR_PALETTE['GENERAL']['HOTKEY']}]y[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] for [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f" and [{COLOR_PALETTE['GENERAL']['COLDKEY']}]n[/{COLOR_PALETTE['GENERAL']['COLDKEY']}] for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]] (default is [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}])", default=False, ) @@ -2438,7 +2439,7 @@ def wallet_sign( wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate ) if not message: - message = typer.prompt("Enter the message to encode and sign") + message = Prompt.ask("Enter the [blue]message[/blue] to encode and sign") return self._run_command(wallets.sign(wallet, message, use_hotkey)) @@ -3226,7 +3227,7 @@ def sudo_set( if not param_value: param_value = Prompt.ask( - f"Enter the new value for [dark_orange]{param_name}[/dark_orange] in the VALUE column format" + f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" ) wallet = self.wallet_ask( @@ -3379,16 +3380,12 @@ def sudo_set_take( current_take = self._run_command( sudo.get_current_take(self.initialize_chain(network), wallet) ) - console.print(f"Current take is [dark_orange]{current_take * 100.:.2f}%") + console.print(f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%") if not take: - max_value_style = typer.style(f"Max: {max_value}", fg="magenta") - min_value_style = typer.style(f"Min: {min_value}", fg="magenta") - prompt_text = typer.style( - "Enter take value (0.18 for 18%)", fg="bright_cyan", bold=True + take = FloatPrompt.ask( + f"Enter [blue]take value[/blue] (0.18 for 18%) [blue]Min: {min_value} Max: {max_value}" ) - take = FloatPrompt.ask(f"{prompt_text} {min_value_style} {max_value_style}") - if not (min_value <= take <= max_value): print_error( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" @@ -3429,7 +3426,7 @@ def sudo_get_take( current_take = self._run_command( sudo.get_current_take(self.initialize_chain(network), wallet) ) - console.print(f"Current take is [dark_orange]{current_take * 100.:.2f}%") + console.print(f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%") def subnets_list( self, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 83f0c9a6..7a6b1132 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -118,7 +118,7 @@ async def set_hyperparameter_extrinsic( return False with console.status( - f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ...", + f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", spinner="earth", ): substrate = subtensor.substrate @@ -167,7 +167,7 @@ async def set_hyperparameter_extrinsic( # Successful registration, final check for membership else: console.print( - f":white_heavy_check_mark: [green]Hyperparameter {parameter} changed to {value}[/green]" + f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" ) return True @@ -409,7 +409,7 @@ async def set_take_extrinsic( if current_take_u16 < take_u16: console.print( - f"Current take is [dark_orange]{current_take * 100.:.2f}%[/dark_orange]. Increasing to [dark_orange]{take * 100:.2f}%." + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Increasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%." ) with console.status( f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..." @@ -426,7 +426,7 @@ async def set_take_extrinsic( else: console.print( - f"Current take is [dark_orange]{current_take * 100.:.2f}%[/dark_orange]. Decreasing to [dark_orange]{take * 100:.2f}%." + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Decreasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%." ) with console.status( f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..." @@ -444,7 +444,7 @@ async def set_take_extrinsic( if not success: err_console.print(err) else: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print(":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]") return success @@ -692,7 +692,7 @@ async def _do_set_take() -> bool: ) if not len(netuids_registered) > 0: err_console.print( - f"Hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered to any subnet. Please register using [dark_orange]`btcli subnets register`[/dark_orange] and try again." + f"Hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] is not registered to any subnet. Please register using [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]`btcli subnets register`[{COLOR_PALETTE['GENERAL']['SUBHEADING']}] and try again." ) return False @@ -708,10 +708,10 @@ async def _do_set_take() -> bool: return False else: new_take = await get_current_take(subtensor, wallet) - console.print(f"New take is [dark_orange]{new_take * 100.:.2f}%") + console.print(f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%") return True - console.print(f"Setting take on [dark_orange]network: {subtensor.network}") + console.print(f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}") try: wallet.unlock_hotkey() diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 0c4d0d4d..b408c7f6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1694,11 +1694,11 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): ) if not use_hotkey: keypair = wallet.coldkey - print_verbose(f"Signing using coldkey: {wallet.name}") + print_verbose(f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}") else: keypair = wallet.hotkey - print_verbose(f"Signing using hotkey: {wallet.hotkey_str}") + print_verbose(f"Signing using [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey: {wallet.hotkey_str}") signed_message = keypair.sign(message.encode("utf-8")).hex() - console.print("[bold green]Message signed successfully:") + console.print("[dark_sea_green3]Message signed successfully:") console.print(signed_message) From 68798fc7618acd667dc6ac1a8f342fb117610711 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 7 Nov 2024 16:32:10 -0800 Subject: [PATCH 137/332] Overhauled identities module --- bittensor_cli/cli.py | 253 ++++++++---------- bittensor_cli/src/bittensor/chain_data.py | 18 ++ .../src/bittensor/subtensor_interface.py | 64 +++-- bittensor_cli/src/bittensor/utils.py | 65 ++++- bittensor_cli/src/commands/subnets.py | 88 +++--- bittensor_cli/src/commands/wallets.py | 244 +++++------------ 6 files changed, 353 insertions(+), 379 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 10b56eab..ba6932b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 import asyncio -import binascii import curses -from functools import partial import os.path import re import ssl @@ -47,6 +45,7 @@ validate_netuid, is_rao_network, get_effective_network, + prompt_for_identity, ) from typing_extensions import Annotated from textwrap import dedent @@ -1213,6 +1212,8 @@ def wallet_ask( console.print( f"Using the [blue]wallet path[/blue] from config:[bold magenta] {wallet_path}" ) + else: + wallet_path = defaults.wallet.path if WO.PATH in ask_for and not wallet_path: wallet_path = Prompt.ask( @@ -1751,7 +1752,8 @@ def wallet_regen_coldkey( if not wallet_name: wallet_name = Prompt.ask( - f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1805,7 +1807,8 @@ def wallet_regen_coldkey_pub( if not wallet_name: wallet_name = Prompt.ask( - f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1920,12 +1923,14 @@ def wallet_new_hotkey( if not wallet_name: wallet_name = Prompt.ask( - f"Enter the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]wallet name", default=defaults.wallet.name + f"Enter the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]wallet name", + default=defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - f"Enter the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", default=defaults.wallet.hotkey + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", + default=defaults.wallet.hotkey, ) wallet = self.wallet_ask( @@ -1975,7 +1980,8 @@ def wallet_new_coldkey( if not wallet_name: wallet_name = Prompt.ask( - f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = self.wallet_ask( @@ -2048,7 +2054,8 @@ def wallet_create_wallet( ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - f"Enter the the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", default=defaults.wallet.hotkey + f"Enter the the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", + default=defaults.wallet.hotkey, ) self.verbosity_handler(quiet, verbose) @@ -2165,11 +2172,11 @@ def wallet_history( # if self.config.get("network"): # if self.config.get("network") != "finney": # console.print(no_use_config_str) - + # For Rao games print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - + self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -2186,69 +2193,37 @@ def wallet_set_id( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, - display_name: str = typer.Option( + name: str = typer.Option( "", - "--display-name", - "--display", + "--name", help="The display name for the identity.", ), - legal_name: str = typer.Option( - "", - "--legal-name", - "--legal", - help="The legal name for the identity.", - ), web_url: str = typer.Option( "", "--web-url", "--web", help="The web URL for the identity.", ), - riot_handle: str = typer.Option( - "", - "--riot-handle", - "--riot", - help="The Riot handle for the identity.", - ), - email: str = typer.Option( - "", - help="The email address for the identity.", - ), - pgp_fingerprint: str = typer.Option( - "", - "--pgp-fingerprint", - "--pgp", - help="The PGP fingerprint for the identity.", - ), image_url: str = typer.Option( "", "--image-url", "--image", help="The image URL for the identity.", ), - info_: str = typer.Option( + discord_handle: str = typer.Option( "", - "--info", - "-i", - help="The info for the identity.", + "--discord", + help="The Discord handle for the identity.", ), - twitter_url: str = typer.Option( + description: str = typer.Option( "", - "-x", - "-𝕏", - "--twitter-url", - "--twitter", - help="The 𝕏 (Twitter) URL for the identity.", + "--description", + help="The description for the identity.", ), - validator_id: Optional[bool] = typer.Option( - None, - "--validator/--not-validator", - help="Are you updating a validator hotkey identity?", - ), - subnet_netuid: Optional[int] = typer.Option( - None, - "--netuid", - help="Netuid if you are updating identity of a subnet owner", + additional_info: str = typer.Option( + "", + "--additional", + help="Additional details for the identity.", ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2276,94 +2251,64 @@ def wallet_set_id( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.HOTKEY, WO.PATH, WO.NAME], - validate=WV.WALLET_AND_HOTKEY, + ask_for=[WO.NAME], + validate=WV.WALLET, ) - if not any( - [ - display_name, - legal_name, - web_url, - riot_handle, - email, - pgp_fingerprint, - image_url, - info_, - twitter_url, - ] - ): - console.print( - "[yellow]All fields are optional. Press Enter to skip a field.[/yellow]" - ) - text_rejection = partial( - retry_prompt, - rejection=lambda x: sys.getsizeof(x) > 113, - rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", - ) - - def pgp_check(s: str): - try: - if s.startswith("0x"): - s = s[2:] # Strip '0x' - pgp_fingerprint_encoded = binascii.unhexlify(s.replace(" ", "")) - except Exception: - return True - return True if len(pgp_fingerprint_encoded) != 20 else False - - display_name = display_name or text_rejection("Display name") - legal_name = legal_name or text_rejection("Legal name") - web_url = web_url or text_rejection("Web URL") - riot_handle = riot_handle or text_rejection("Riot handle") - email = email or text_rejection("Email address") - pgp_fingerprint = pgp_fingerprint or retry_prompt( - "PGP fingerprint (Eg: A1B2 C3D4 E5F6 7890 1234 5678 9ABC DEF0 1234 5678)", - lambda s: False if not s else pgp_check(s), - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes.", - ) - image_url = image_url or text_rejection("Image URL") - info_ = info_ or text_rejection("Enter info") - twitter_url = twitter_url or text_rejection("𝕏 (Twitter) URL") - - validator_id = validator_id or Confirm.ask( - "Are you updating a [bold blue]validator hotkey[/bold blue] identity or a [bold blue]subnet " - "owner[/bold blue] identity?\n" - "Enter [bold green]Y[/bold green] for [bold]validator hotkey[/bold] or [bold red]N[/bold red] for " - "[bold]subnet owner[/bold]", - show_choices=True, - ) - - if validator_id is False: - subnet_netuid = IntPrompt.ask("Enter the netuid of the subnet you own") + current_identity = self._run_command( + wallets.get_id( + self.initialize_chain(network), + wallet.coldkeypub.ss58_address, + "Current on-chain identity", + ) + ) + + if prompt: + if not Confirm.ask( + "Cost to register an [blue]Identity[/blue] is [blue]0.1 TAO[/blue]," + " are you sure you wish to continue?" + ): + console.print(":cross_mark: Aborted!") + raise typer.Exit() + + identity = prompt_for_identity( + current_identity, + name, + web_url, + image_url, + discord_handle, + description, + additional_info, + ) return self._run_command( wallets.set_id( wallet, self.initialize_chain(network), - display_name, - legal_name, - web_url, - pgp_fingerprint, - riot_handle, - email, - image_url, - twitter_url, - info_, - validator_id, - subnet_netuid, + identity["name"], + identity["url"], + identity["image"], + identity["discord"], + identity["description"], + identity["additional"], prompt, ) ) def wallet_get_id( self, - target_ss58_address: str = typer.Option( + wallet_name: Optional[str] = Options.wallet_name, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_path: Optional[str] = Options.wallet_path, + coldkey_ss58=typer.Option( None, + "--ss58", + "--coldkey_ss58", + "--coldkey.ss58_address", + "--coldkey.ss58", "--key", "-k", - "--ss58", - help="The coldkey or hotkey ss58 address to query.", - prompt=True, + help="Coldkey address of the wallet", ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, @@ -2386,13 +2331,28 @@ def wallet_get_id( [bold]Note[/bold]: This command is primarily used for informational purposes and has no side effects on the blockchain network state. """ - if not is_valid_ss58_address(target_ss58_address): - print_error("You have entered an incorrect ss58 address. Please try again") - raise typer.Exit() + wallet = None + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error("You entered an invalid ss58 address") + raise typer.Exit() + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if is_valid_ss58_address(coldkey_or_ss58): + coldkey_ss58 = coldkey_or_ss58 + else: + wallet_name = coldkey_or_ss58 if coldkey_or_ss58 else wallet_name + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME] + ) + coldkey_ss58 = wallet.coldkeypub.ss58_address self.verbosity_handler(quiet, verbose) return self._run_command( - wallets.get_id(self.initialize_chain(network), target_ss58_address) + wallets.get_id(self.initialize_chain(network), coldkey_ss58) ) def wallet_sign( @@ -3380,7 +3340,9 @@ def sudo_set_take( current_take = self._run_command( sudo.get_current_take(self.initialize_chain(network), wallet) ) - console.print(f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%") + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) if not take: take = FloatPrompt.ask( @@ -3426,7 +3388,9 @@ def sudo_get_take( current_take = self._run_command( sudo.get_current_take(self.initialize_chain(network), wallet) ) - console.print(f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%") + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) def subnets_list( self, @@ -3541,13 +3505,32 @@ def subnets_create( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, + ask_for=[ + WO.NAME, + ], + validate=WV.WALLET, ) - return self._run_command( + success = self._run_command( subnets.create(wallet, self.initialize_chain(network), prompt) ) + if success and prompt: + set_id = Confirm.ask( + "[dark_sea_green3]Do you want to set/update your identity?", + default=False, + show_default=True, + ) + if set_id: + self.wallet_set_id( + wallet_name=wallet.name, + wallet_hotkey=wallet.hotkey, + wallet_path=wallet.path, + network=network, + prompt=prompt, + quiet=quiet, + verbose=verbose, + ) + def subnets_pow_register( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index bee31608..9465edda 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -78,6 +78,24 @@ def decode_account_id(account_id_bytes: tuple): return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) +def decode_hex_identity(info_dictionary): + decoded_info = {} + for k, v in info_dictionary.items(): + if isinstance(v, dict): + item = next(iter(v.values())) + else: + item = v + + if isinstance(item, tuple): + try: + decoded_info[k] = bytes(item).decode() + except UnicodeDecodeError: + print(f"Could not decode: {k}: {item}") + else: + decoded_info[k] = item + return decoded_info + + def process_stake_data(stake_data): decoded_stake_data = {} for account_id_bytes, stake_ in stake_data: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 0fa1fc9d..6fbc4c14 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -24,6 +24,7 @@ NeuronInfo, SubnetHyperparameters, decode_account_id, + decode_hex_identity, DelegateInfoLite, DynamicInfo, ) @@ -819,6 +820,36 @@ async def get_delegated( return DelegateInfo.delegated_list_from_vec_u8(bytes_result) + async def query_all_identities( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, dict]: + """ + Queries all identities on the Bittensor blockchain. + + :param block_hash: The hash of the blockchain block number at which to perform the query. + :param reuse_block: Whether to reuse the last-used blockchain block hash. + + :return: A dictionary mapping addresses to their decoded identity data. + """ + + identities = await self.substrate.query_map( + module="SubtensorModule", + storage_function="Identities", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if identities is None: + return {} + + all_identities = { + decode_account_id(ss58_address[0]): decode_hex_identity(identity) + for ss58_address, identity in identities + } + return all_identities + async def query_identity( self, key: str, @@ -843,40 +874,17 @@ async def query_identity( 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. """ - - def decode_hex_identity_dict(info_dictionary): - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - if isinstance(item, tuple) and item: - if len(item) > 1: - try: - info_dictionary[k] = ( - bytes(item).hex(sep=" ", bytes_per_sep=2).upper() - ) - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - try: - info_dictionary[k] = bytes(item[0]).decode("utf-8") - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - info_dictionary[k] = item - - return info_dictionary - identity_info = await self.substrate.query( - module="Registry", - storage_function="IdentityOf", + module="SubtensorModule", + storage_function="Identities", params=[key], block_hash=block_hash, reuse_block_hash=reuse_block, ) + if not identity_info: + return {} try: - return decode_hex_identity_dict(identity_info["info"]) + return decode_hex_identity(identity_info) except TypeError: return {} diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index ffe319d1..cc47de9f 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -3,9 +3,11 @@ import os import sqlite3 import webbrowser +import sys from pathlib import Path from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable from urllib.parse import urlparse +from functools import partial from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT @@ -16,6 +18,7 @@ import numpy as np from numpy.typing import NDArray from rich.console import Console +from rich.prompt import Prompt import scalecodec from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset @@ -954,7 +957,7 @@ def retry_prompt( rejection_text: str, default="", show_default=False, - prompt_type=typer.prompt, + prompt_type=Prompt.ask, ): """ Allows for asking prompts again if they do not meet a certain criteria (as defined in `rejection`) @@ -1001,9 +1004,9 @@ def get_effective_network(config, network: Optional[list[str]]) -> str: else: return defaults.subtensor.network + def is_rao_network(network: str) -> bool: """Check if the given network is 'rao'.""" - network = network.lower() rao_identifiers = [ "rao", @@ -1014,3 +1017,61 @@ def is_rao_network(network: str) -> bool: or network in rao_identifiers or "rao.chain.opentensor.ai" in network ) + + +def prompt_for_identity( + current_identity: dict, + name: Optional[str], + web_url: Optional[str], + image_url: Optional[str], + discord_handle: Optional[str], + description: Optional[str], + additional_info: Optional[str], +): + """ + Prompts the user for identity fields with validation. + Returns a dictionary with the updated fields. + """ + identity_fields = {} + + fields = [ + ("name", "[blue]Display name[/blue]", name), + ("url", "[blue]Web URL[/blue]", web_url), + ("image", "[blue]Image URL[/blue]", image_url), + ("discord", "[blue]Discord handle[/blue]", discord_handle), + ("description", "[blue]Description[/blue]", description), + ("additional", "[blue]Additional information[/blue]", additional_info), + ] + + text_rejection = partial( + retry_prompt, + rejection=lambda x: sys.getsizeof(x) > 113, + rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", + ) + + if not any( + [ + name, + web_url, + image_url, + discord_handle, + description, + additional_info, + ] + ): + console.print( + "[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n" + "[dark_sea_green3]Tip: Entering a space and pressing Enter will clear existing default value.\n" + ) + + for key, prompt, value in fields: + if value: + identity_fields[key] = value + else: + identity_fields[key] = text_rejection( + prompt, + default=current_identity.get(key, ""), + show_default=True, + ) + + return identity_fields diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index b9ad3177..ec066b9e 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -2,6 +2,7 @@ import json import sqlite3 from typing import TYPE_CHECKING, Optional, cast +import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError @@ -22,7 +23,7 @@ ) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph -from bittensor_cli.src.commands.wallets import set_id, set_id_prompts +from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, console, @@ -35,6 +36,7 @@ millify, render_table, update_metadata_table, + prompt_for_identity, ) if TYPE_CHECKING: @@ -153,7 +155,7 @@ async def _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) console.print( - f":white_heavy_check_mark: [{COLOR_PALETTE['GENERAL']['SUCCESS']}]Registered subnetwork with netuid: {attributes[0]}" + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) return True @@ -172,10 +174,11 @@ async def subnets_list( async def fetch_subnet_data(): subnets = await subtensor.get_all_subnet_dynamic_info() - global_weights = await subtensor.get_global_weights( - [subnet.netuid for subnet in subnets] + global_weights, identities = await asyncio.gather( + subtensor.get_global_weights([subnet.netuid for subnet in subnets]), + subtensor.query_all_identities(), ) - return subnets, global_weights + return subnets, global_weights, identities def define_table(total_emissions: float, total_rate: float): table = Table( @@ -191,20 +194,21 @@ def define_table(total_emissions: float, total_rate: float): pad_edge=True, ) - table.add_column("[bold white]NETUID", style="grey89", justify="center") + table.add_column("[bold white]Netuid", style="grey89", justify="center") table.add_column( - "[bold white]SYMBOL", + "[bold white]Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="right", ) + table.add_column("[bold white]Owner", style="cyan", justify="left") table.add_column( - f"[bold white]EMISSION ({Balance.get_unit(0)})", + f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="left", footer=f"τ {total_emissions:.4f}", ) table.add_column( - f"[bold white]STAKE ({Balance.get_unit(1)}_out)", + f"[bold white]Stake ({Balance.get_unit(1)}_out)", style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) @@ -236,7 +240,7 @@ def define_table(total_emissions: float, total_rate: float): return table # Non-live mode - def create_table(subnets, global_weights): + def create_table(subnets, global_weights, identities): rows = [] for subnet in subnets: netuid = subnet.netuid @@ -245,12 +249,15 @@ def create_table(subnets, global_weights): if netuid == 0: emission_tao = 0.0 + identity = "~" else: emission_tao = subnet.emission.tao + identity = identities.get(subnet.owner, {}).get("name", "~") - # Prepare content + # Prepare cells netuid_cell = str(netuid) symbol_cell = f"{subnet.symbol}" + identity_cell = identity emission_cell = f"{emission_tao:,.4f}" alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" tao_in_cell = f"{subnet.tao_in.tao:,.4f} τ" @@ -265,6 +272,7 @@ def create_table(subnets, global_weights): ( netuid_cell, # Netuid symbol_cell, # Symbol + identity_cell, # Identity emission_cell, # Emission (τ) alpha_out_cell, # Stake α_out tao_in_cell, # TAO Pool τ_in @@ -285,16 +293,15 @@ def create_table(subnets, global_weights): # Sort rows by emission, keeping the root subnet in the first position sorted_rows = [rows[0]] + sorted( - rows[1:], key=lambda x: float(str(x[2]).replace(",", "")), reverse=True + rows[1:], key=lambda x: float(str(x[3]).replace(",", "")), reverse=True ) - # Add rows to the table for row in sorted_rows: table.add_row(*row) return table # Live mode - def create_table_live(subnets, global_weights, previous_data): + def create_table_live(subnets, global_weights, identities, previous_data): def format_cell(value, previous_value, unit="", precision=4): if previous_value is not None: change = value - previous_value @@ -322,8 +329,10 @@ def format_cell(value, previous_value, unit="", precision=4): if netuid == 0: emission_tao = 0.0 + identity = "~" else: emission_tao = subnet.emission.tao + identity = identities.get(subnet.owner, {}).get("name", "~") # Store current values for comparison current_data[netuid] = { @@ -335,12 +344,11 @@ def format_cell(value, previous_value, unit="", precision=4): "blocks_since_last_step": subnet.blocks_since_last_step, "global_weight": global_weight, } - - # Retrieve previous data if available prev = previous_data.get(netuid) if previous_data else {} - # Prepare content + # Prepare cells netuid_cell = str(netuid) + identity_cell = identity symbol_cell = f"{subnet.symbol}" emission_cell = format_cell( emission_tao, prev.get("emission_tao"), unit="", precision=4 @@ -364,7 +372,7 @@ def format_cell(value, previous_value, unit="", precision=4): subnet.price.tao, prev.get("price"), unit=f" τ/{symbol}", precision=4 ) - # Content: Blocks_since_last_step + # Tempo cell prev_blocks_since_last_step = prev.get("blocks_since_last_step") if prev_blocks_since_last_step is not None: if subnet.blocks_since_last_step >= prev_blocks_since_last_step: @@ -388,7 +396,7 @@ def format_cell(value, previous_value, unit="", precision=4): f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" ) - # Content: Global_weight + # Local weight coeff cell prev_global_weight = prev.get("global_weight") if prev_global_weight is not None and global_weight is not None: weight_change = float(global_weight) - float(prev_global_weight) @@ -415,6 +423,7 @@ def format_cell(value, previous_value, unit="", precision=4): ( netuid_cell, # Netuid symbol_cell, # Symbol + identity_cell, # Identity emission_cell, # Emission (τ) alpha_out_cell, # Stake α_out tao_in_cell, # TAO Pool τ_in @@ -436,11 +445,9 @@ def format_cell(value, previous_value, unit="", precision=4): # Sort rows by emission, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted( rows[1:], - key=lambda x: float(str(x[2]).split()[0].replace(",", "")), + key=lambda x: float(str(x[3]).split()[0].replace(",", "")), reverse=True, ) - - # Add rows to the table for row in sorted_rows: table.add_row(*row) return table, current_data @@ -462,18 +469,15 @@ def format_cell(value, previous_value, unit="", precision=4): with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, global_weights = await fetch_subnet_data() + subnets, global_weights, identities = await fetch_subnet_data() table, current_data = create_table_live( - subnets, global_weights, previous_data + subnets, global_weights, identities, previous_data ) previous_data = current_data progress.reset(progress_task) start_time = asyncio.get_event_loop().time() - - # Create the message message = "\nLive view active. Press [bold red]Ctrl + C[/bold red] to exit" - # Include the message in the live render group live_render = Group(table, progress, message) live.update(live_render) while not progress.finished: @@ -485,8 +489,8 @@ def format_cell(value, previous_value, unit="", precision=4): pass # Ctrl + C else: # Non-live mode - subnets, global_weights = await fetch_subnet_data() - table = create_table(subnets, global_weights) + subnets, global_weights, identities = await fetch_subnet_data() + table = create_table(subnets, global_weights, identities) console.print(table) display_table = Prompt.ask( @@ -853,8 +857,30 @@ async def create(wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool): ) if do_set_identity: - id_prompts = set_id_prompts(validator=False) - await set_id(wallet, subtensor, *id_prompts, prompt=prompt) + current_identity = await get_id( + subtensor, wallet.coldkeypub.ss58_address, "Current on-chain identity" + ) + if prompt: + if not Confirm.ask( + "\nCost to register an [blue]Identity[/blue] is [blue]0.1 TAO[/blue]," + " are you sure you wish to continue?" + ): + console.print(":cross_mark: Aborted!") + raise typer.Exit() + + identity = prompt_for_identity(current_identity=current_identity) + + await set_id( + wallet, + subtensor, + identity["name"], + identity["url"], + identity["image"], + identity["discord"], + identity["description"], + identity["additional"], + prompt, + ) async def pow_register( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b408c7f6..3f59ae11 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1413,213 +1413,90 @@ async def swap_hotkey( ) -def set_id_prompts( - validator: bool, -) -> tuple[str, str, str, str, str, str, str, str, str, bool, int]: - """ - Used to prompt the user to input their info for setting the ID - :return: (display_name, legal_name, web_url, riot_handle, email,pgp_fingerprint, image_url, info_, twitter_url, - validator_id) - """ - text_rejection = partial( - retry_prompt, - rejection=lambda x: sys.getsizeof(x) > 113, - rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", - ) +def create_identity_table(title: str = None): + if not title: + title = "On-Chain Identity" - def pgp_check(s: str): - try: - if s.startswith("0x"): - s = s[2:] # Strip '0x' - pgp_fingerprint_encoded = binascii.unhexlify(s.replace(" ", "")) - except Exception: - return True - return True if len(pgp_fingerprint_encoded) != 20 else False - - display_name = text_rejection("Display name") - legal_name = text_rejection("Legal name") - web_url = text_rejection("Web URL") - riot_handle = text_rejection("Riot handle") - email = text_rejection("Email address") - pgp_fingerprint = retry_prompt( - "PGP fingerprint (Eg: A1B2 C3D4 E5F6 7890 1234 5678 9ABC DEF0 1234 5678)", - lambda s: False if not s else pgp_check(s), - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes.", - ) - image_url = text_rejection("Image URL") - info_ = text_rejection("Enter info") - twitter_url = text_rejection("𝕏 (Twitter) URL") - - subnet_netuid = None - if validator is False: - subnet_netuid = IntPrompt.ask("Enter the netuid of the subnet you own") - - return ( - display_name, - legal_name, - web_url, - pgp_fingerprint, - riot_handle, - email, - image_url, - twitter_url, - info_, - validator, - subnet_netuid, + table = Table( + Column("Item", justify="right", style=COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN'], no_wrap=True), + Column("Value", style=COLOR_PALETTE['GENERAL']['SUBHEADING']), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, ) + return table async def set_id( wallet: Wallet, subtensor: SubtensorInterface, - display_name: str, - legal_name: str, + name: str, web_url: str, - pgp_fingerprint: str, - riot_handle: str, - email: str, - image: str, - twitter: str, - info_: str, - validator_id: bool, - subnet_netuid: int, + image_url: str, + discord_handle: str, + description: str, + additional_info: str, prompt: bool, ): """Create a new or update existing identity on-chain.""" - id_dict = { - "additional": [[]], - "display": display_name, - "legal": legal_name, - "web": web_url, - "pgp_fingerprint": pgp_fingerprint, - "riot": riot_handle, - "email": email, - "image": image, - "twitter": twitter, - "info": info_, + identity_data = { + "name": name.encode(), + "url": web_url.encode(), + "image": image_url.encode(), + "discord": discord_handle.encode(), + "description": description.encode(), + "additional": additional_info.encode(), } - try: - pgp_fingerprint_encoded = binascii.unhexlify(pgp_fingerprint.replace(" ", "")) - except Exception as e: - print_error(f"The PGP is not in the correct format: {e}") - raise typer.Exit() - - for field, string in id_dict.items(): - if ( - field == "pgp_fingerprint" - and pgp_fingerprint - and len(pgp_fingerprint_encoded) != 20 - ): - err_console.print( - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes." - ) - return False - elif (size := getsizeof(string)) > 113: # 64 + 49 overhead bytes for string + for field, value in identity_data.items(): + max_size = 64 # bytes + if len(value) > max_size: err_console.print( - f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= 64 raw bytes.\n" - f"Value: '{string}' currently [white]{size} bytes[/white]." + f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" + f"Value '{value.decode()}' is {len(value)} bytes." ) return False - identified = ( - wallet.hotkey.ss58_address if validator_id else wallet.coldkeypub.ss58_address - ) - encoded_id_dict = { - "info": { - "additional": [[]], - "display": {f"Raw{len(display_name.encode())}": display_name.encode()}, - "legal": {f"Raw{len(legal_name.encode())}": legal_name.encode()}, - "web": {f"Raw{len(web_url.encode())}": web_url.encode()}, - "riot": {f"Raw{len(riot_handle.encode())}": riot_handle.encode()}, - "email": {f"Raw{len(email.encode())}": email.encode()}, - "pgp_fingerprint": pgp_fingerprint_encoded if pgp_fingerprint else None, - "image": {f"Raw{len(image.encode())}": image.encode()}, - "info": {f"Raw{len(info_.encode())}": info_.encode()}, - "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, - }, - "identified": identified, - } - - if prompt: - if not Confirm.ask( - "Cost to register an Identity is [bold white italic]0.1 Tao[/bold white italic]," - " are you sure you wish to continue?" - ): - console.print(":cross_mark: Aborted!") - raise typer.Exit() - - if validator_id: - block_hash = await subtensor.substrate.get_chain_head() - - is_registered_on_root, hotkey_owner = await asyncio.gather( - is_hotkey_registered( - subtensor, netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ), - subtensor.get_hotkey_owner( - hotkey_ss58=wallet.hotkey.ss58_address, block_hash=block_hash - ), - ) - - if not is_registered_on_root: - print_error("The hotkey is not registered on root. Aborting.") - return False - - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - print_error("The hotkey doesn't belong to the coldkey wallet. Aborting.") - return False - else: - subnet_owner_ = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="SubnetOwner", - params=[subnet_netuid], - ) - subnet_owner = decode_account_id(subnet_owner_[0]) - if subnet_owner != wallet.coldkeypub.ss58_address: - print_error(f":cross_mark: This wallet doesn't own subnet {subnet_netuid}.") - return False - try: wallet.unlock_coldkey() except KeyFileError: err_console.print("Error decrypting coldkey (possibly incorrect password)") return False + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_identity", + call_params=identity_data, + ) + with console.status( - ":satellite: [bold green]Updating identity on-chain...", spinner="earth" + " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): - call = await subtensor.substrate.compose_call( - call_module="Registry", - call_function="set_identity", - call_params=encoded_id_dict, - ) success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") return - console.print(":white_heavy_check_mark: Success!") - identity = await subtensor.query_identity( - identified or wallet.coldkey.ss58_address - ) + console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) - table = Table( - Column("Key", justify="right", style="cyan", no_wrap=True), - Column("Value", style="magenta"), - title="[bold white italic]Updated On-Chain Identity", - ) - - table.add_row("Address", identified or wallet.coldkey.ss58_address) + table = create_identity_table(title="New on-chain Identity") + table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "~") + table.add_row(key, str(value) if value else "~") return console.print(table) -async def get_id(subtensor: SubtensorInterface, ss58_address: str): +async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = None): with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): @@ -1627,22 +1504,19 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str): if not identity: err_console.print( - f"[red]Identity not found[/red]" - f" for [light_goldenrod3]{ss58_address}[/light_goldenrod3]" - f" on [white]{subtensor}[/white]" + f"[red]Existing identity not found[/red]" + f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f" on {subtensor}" ) - return - table = Table( - Column("Item", justify="right", style="cyan", no_wrap=True), - Column("Value", style="magenta"), - title="[bold white italic]On-Chain Identity", - ) - + return {} + + table = create_identity_table(title) table.add_row("Address", ss58_address) for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "~") + table.add_row(key, str(value) if value else "~") - return console.print(table) + console.print(table) + return identity async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): @@ -1694,10 +1568,14 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): ) if not use_hotkey: keypair = wallet.coldkey - print_verbose(f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}") + print_verbose( + f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}" + ) else: keypair = wallet.hotkey - print_verbose(f"Signing using [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey: {wallet.hotkey_str}") + print_verbose( + f"Signing using [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey: {wallet.hotkey_str}" + ) signed_message = keypair.sign(message.encode("utf-8")).hex() console.print("[dark_sea_green3]Message signed successfully:") From 0f2df53ee11ea3d004b9abc5d88b9ec3e126c477 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 7 Nov 2024 16:45:47 -0800 Subject: [PATCH 138/332] ui improvements + small fix --- bittensor_cli/src/bittensor/utils.py | 2 +- bittensor_cli/src/commands/subnets.py | 12 ++++++++++-- bittensor_cli/src/commands/wallets.py | 13 +++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index cc47de9f..95cf5371 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1060,7 +1060,7 @@ def prompt_for_identity( ] ): console.print( - "[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n" + "\n[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n" "[dark_sea_green3]Tip: Entering a space and pressing Enter will clear existing default value.\n" ) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index ec066b9e..c4ab7cbb 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -853,7 +853,7 @@ async def create(wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool): if success and prompt: # Prompt for user to set identity. do_set_identity = Confirm.ask( - "Subnetwork registered successfully. Would you like to set your identity?" + "Would you like to set your [blue]identity?[/blue]" ) if do_set_identity: @@ -868,7 +868,15 @@ async def create(wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool): console.print(":cross_mark: Aborted!") raise typer.Exit() - identity = prompt_for_identity(current_identity=current_identity) + identity = prompt_for_identity( + current_identity=current_identity, + name=None, + web_url=None, + image_url=None, + discord_handle=None, + description=None, + additional_info=None, + ) await set_id( wallet, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3f59ae11..5def5405 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1418,8 +1418,13 @@ def create_identity_table(title: str = None): title = "On-Chain Identity" table = Table( - Column("Item", justify="right", style=COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN'], no_wrap=True), - Column("Value", style=COLOR_PALETTE['GENERAL']['SUBHEADING']), + Column( + "Item", + justify="right", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}", show_footer=True, show_edge=False, @@ -1504,12 +1509,12 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = if not identity: err_console.print( - f"[red]Existing identity not found[/red]" + f"[blue]Existing identity not found[/blue]" f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f" on {subtensor}" ) return {} - + table = create_identity_table(title) table.add_row("Address", ss58_address) for key, value in identity.items(): From 4197af3b3ef2de7d9289a8ae0511abc4efa7f7b1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 8 Nov 2024 10:14:36 -0800 Subject: [PATCH 139/332] Adds root register call for sn 0 --- bittensor_cli/src/commands/subnets.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index c4ab7cbb..09f1d3f2 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -21,6 +21,7 @@ register_extrinsic, burned_register_extrinsic, ) +from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph from bittensor_cli.src.commands.wallets import set_id, get_id @@ -1011,13 +1012,16 @@ async def register( ): return - await burned_register_extrinsic( - subtensor, - wallet=wallet, - netuid=netuid, - prompt=False, - old_balance=balance, - ) + if netuid == 0: + await root_register_extrinsic(subtensor, wallet=wallet) + else: + await burned_register_extrinsic( + subtensor, + wallet=wallet, + netuid=netuid, + prompt=False, + old_balance=balance, + ) # TODO: Confirm emissions, incentive, Dividends are to be fetched from subnet_state or keep NeuronInfo From f621ad84dbb851275d7061e0ee6e9dfc46761015 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 20 Nov 2024 16:42:24 -0800 Subject: [PATCH 140/332] Adds interactive in unstake to select hotkeys/netuids a coldkey is staked to --- bittensor_cli/cli.py | 73 +-- .../bittensor/async_substrate_interface.py | 51 +- .../src/bittensor/subtensor_interface.py | 45 +- bittensor_cli/src/commands/stake/stake.py | 470 ++++++++++++------ bittensor_cli/src/commands/subnets.py | 12 +- 5 files changed, 446 insertions(+), 205 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ba6932b5..f7ae8f20 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2679,6 +2679,12 @@ def stake_remove( "hotkeys in `--include-hotkeys`.", ), prompt: bool = Options.prompt, + interactive: bool = typer.Option( + False, + "--interactive", + "-i", + help="Enter interactive mode for unstaking.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2694,33 +2700,38 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) - netuid = get_optional_netuid(netuid, all_netuids) - if all_hotkeys and include_hotkeys: + if interactive and any( + [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] + ): err_console.print( - "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" - "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." + "Interactive mode cannot be used with hotkey selection options like --include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) raise typer.Exit() - if include_hotkeys and exclude_hotkeys: - err_console.print( - "You have specified both including and excluding hotkeys options. Select one or the other." - ) - raise typer.Exit() + if not interactive: + netuid = get_optional_netuid(netuid, all_netuids) + if all_hotkeys and include_hotkeys: + err_console.print( + "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" + " should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." + ) + raise typer.Exit() - if unstake_all and amount: - err_console.print( - "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." - ) - raise typer.Exit() + if include_hotkeys and exclude_hotkeys: + err_console.print( + "You have specified both including and excluding hotkeys options. Select one or the other." + ) + raise typer.Exit() - # TODO: We are prompting for amount for each subnet later - confirm this to be removed - # if not unstake_all and not amount and not keep_stake: - # amount = FloatPrompt.ask("[blue bold]Amount to unstake (TAO τ)[/blue bold]") + if unstake_all and amount: + err_console.print( + "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." + ) + raise typer.Exit() - if unstake_all and not amount: - if not Confirm.ask("Unstake all staked TAO tokens?", default=False): + if amount and amount <= 0: + print_error(f"You entered an incorrect unstake amount: {amount}") raise typer.Exit() if ( @@ -2728,6 +2739,7 @@ def stake_remove( and not hotkey_ss58_address and not all_hotkeys and not include_hotkeys + and not interactive ): if not wallet_name: wallet_name = Prompt.ask( @@ -2735,7 +2747,7 @@ def stake_remove( default=self.config.get("wallet_name") or defaults.wallet.name, ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from", default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) if is_valid_ss58_address(hotkey_or_ss58): @@ -2753,7 +2765,13 @@ def stake_remove( validate=WV.WALLET_AND_HOTKEY, ) - elif all_hotkeys or include_hotkeys or exclude_hotkeys or hotkey_ss58_address: + elif ( + all_hotkeys + or include_hotkeys + or exclude_hotkeys + or hotkey_ss58_address + or interactive + ): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) @@ -2771,8 +2789,8 @@ def stake_remove( included_hotkeys = parse_to_list( include_hotkeys, str, - "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", - is_ss58=True, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", + is_ss58=False, ) else: included_hotkeys = [] @@ -2781,16 +2799,12 @@ def stake_remove( excluded_hotkeys = parse_to_list( exclude_hotkeys, str, - "Hotkeys must be a comma-separated list of ss58s, e.g., `--exclude-hotkeys 5Grw....,5Grw....`.", - is_ss58=True, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", + is_ss58=False, ) else: excluded_hotkeys = [] - if amount and amount <= 0: - print_error(f"You entered an incorrect unstake amount: {amount}") - raise typer.Exit() - return self._run_command( stake.unstake( wallet, @@ -2804,6 +2818,7 @@ def stake_remove( keep_stake, unstake_all, prompt, + interactive, ) ) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index ebe0e9f5..bd6bbb98 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -4,7 +4,7 @@ from collections import defaultdict from dataclasses import dataclass from hashlib import blake2b -from typing import Optional, Any, Union, Callable, Awaitable, cast, Iterable +from typing import Optional, Any, Union, Callable, Awaitable, cast from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 from async_property import async_property @@ -460,6 +460,9 @@ def __init__(self, chain, runtime_config, metadata, type_registry): self.runtime_config = runtime_config self.metadata = metadata + def __str__(self): + return f"Runtime: {self.chain} | {self.config}" + @property def implements_scaleinfo(self) -> bool: """ @@ -897,9 +900,10 @@ async def init_runtime( async def get_runtime(block_hash, block_id) -> Runtime: # Check if runtime state already set to current block - if (block_hash and block_hash == self.last_block_hash) or ( - block_id and block_id == self.block_id - ): + if ( + (block_hash and block_hash == self.last_block_hash) + or (block_id and block_id == self.block_id) + ) and self.metadata is not None: return Runtime( self.chain, self.runtime_config, @@ -945,9 +949,11 @@ async def get_runtime(block_hash, block_id) -> Runtime: raise SubstrateRequestException( f"No runtime information for block '{block_hash}'" ) - # Check if runtime state already set to current block - if runtime_info.get("specVersion") == self.runtime_version: + if ( + runtime_info.get("specVersion") == self.runtime_version + and self.metadata is not None + ): return Runtime( self.chain, self.runtime_config, @@ -962,16 +968,19 @@ async def get_runtime(block_hash, block_id) -> Runtime: if self.runtime_version in self.__metadata_cache: # Get metadata from cache # self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - self.metadata = self.__metadata_cache[self.runtime_version] + metadata = self.metadata = self.__metadata_cache[ + self.runtime_version + ] else: - self.metadata = await self.get_block_metadata( + metadata = self.metadata = await self.get_block_metadata( block_hash=runtime_block_hash, decode=True ) # self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) # Update metadata cache self.__metadata_cache[self.runtime_version] = self.metadata - + else: + metadata = self.metadata # Update type registry self.reload_type_registry(use_remote_preset=False, auto_discover=True) @@ -1012,7 +1021,10 @@ async def get_runtime(block_hash, block_id) -> Runtime: if block_id and block_hash: raise ValueError("Cannot provide block_hash and block_id at the same time") - if not (runtime := self.runtime_cache.retrieve(block_id, block_hash)): + if ( + not (runtime := self.runtime_cache.retrieve(block_id, block_hash)) + or runtime.metadata is None + ): runtime = await get_runtime(block_hash, block_id) self.runtime_cache.add_item(block_id, block_hash, runtime) return runtime @@ -1123,7 +1135,7 @@ async def create_storage_key( ------- StorageKey """ - await self.init_runtime(block_hash=block_hash) + runtime = await self.init_runtime(block_hash=block_hash) return StorageKey.create_from_storage_function( pallet, @@ -1603,6 +1615,7 @@ async def _make_rpc_request( result_handler: Optional[ResultHandler] = None, ) -> RequestManager.RequestResults: request_manager = RequestManager(payloads) + subscription_added = False async with self.ws as ws: @@ -1785,23 +1798,13 @@ async def query_multiple( # By allowing for specifying the block hash, users, if they have multiple query types they want # to do, can simply query the block hash first, and then pass multiple query_subtensor calls # into an asyncio.gather, with the specified block hash - if len(params) != len(set(params)): - raise SubstrateRequestException( - "You are attempting to query multiple values, but you have duplicates." - ) - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) if block_hash: self.last_block_hash = block_hash runtime = await self.init_runtime(block_hash=block_hash) preprocessed: tuple[Preprocessed] = await asyncio.gather( *[ - self._preprocess( - [x] if not isinstance(x, Iterable) else list(x), - block_hash, - storage_function, - module, - ) + self._preprocess([x], block_hash, storage_function, module) for x in params ] ) @@ -1809,10 +1812,10 @@ async def query_multiple( self.make_payload(item.queryable, item.method, item.params) for item in preprocessed ] - # These will always be the same throughout the preprocessed list, so we just grab the first one value_scale_type = preprocessed[0].value_scale_type storage_item = preprocessed[0].storage_item + responses = await self._make_rpc_request( all_info, value_scale_type, storage_item, runtime ) @@ -2281,7 +2284,7 @@ async def get_metadata_constant(self, module_name, constant_name, block_hash=Non MetadataModuleConstants """ - # await self.init_runtime(block_hash=block_hash) + await self.init_runtime(block_hash=block_hash) for module in self.metadata.pallets: if module_name == module.name and module.constants: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 6fbc4c14..87f3c250 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -845,9 +845,9 @@ async def query_all_identities( return {} all_identities = { - decode_account_id(ss58_address[0]): decode_hex_identity(identity) - for ss58_address, identity in identities - } + decode_account_id(ss58_address[0]): decode_hex_identity(identity) + for ss58_address, identity in identities + } return all_identities async def query_identity( @@ -888,6 +888,45 @@ async def query_identity( except TypeError: return {} + async def fetch_coldkey_hotkey_identities( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, dict]: + """ + Builds a dictionary containing coldkeys and hotkeys with their associated identities and relationships. + :param block_hash: The hash of the blockchain block number for the query. + :param reuse_block: Whether to reuse the last-used blockchain block hash. + :return: Dict with 'coldkeys' and 'hotkeys' as keys. + """ + + coldkey_identities = await self.query_all_identities() + query = await self.substrate.query_multiple( + params=[(ss58) for ss58, _ in coldkey_identities.items()], + module="SubtensorModule", + storage_function="OwnedHotkeys", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + identities = {"coldkeys": {}, "hotkeys": {}} + for coldkey_ss58, hotkeys in query.items(): + coldkey_identity = coldkey_identities.get(coldkey_ss58) + hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []] + + identities["coldkeys"][coldkey_ss58] = { + "identity": coldkey_identity, + "hotkeys": hotkeys, + } + + for hotkey_ss58 in hotkeys: + identities["hotkeys"][hotkey_ss58] = { + "coldkey": coldkey_ss58, + "identity": coldkey_identity, + } + + return identities + async def weights( self, netuid: int, block_hash: Optional[str] = None ) -> list[tuple[int, list[tuple[int, int]]]]: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index aefa7f52..b5f1c01e 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -17,19 +17,14 @@ from bittensor_cli.src.bittensor.utils import ( # TODO add back in caching console, - create_table, err_console, print_verbose, print_error, - get_coldkey_wallets_for_path, get_hotkey_wallets_for_wallet, is_valid_ss58_address, - get_metadata_table, - update_metadata_table, - render_tree, u16_normalized_float, - validate_coldkey_presence, format_error_message, + group_subnets, ) if TYPE_CHECKING: @@ -1157,7 +1152,207 @@ async def send_extrinsic( await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block -# TODO: Decouple stuff from here. Works but is a mess +async def unstake_selection( + subtensor: "SubtensorInterface", + wallet: Wallet, + dynamic_info, + identities, + old_identities, +): + stake_infos = await subtensor.get_stake_info_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + + if not stake_infos: + print_error("You have no stakes to unstake.") + return + + hotkey_stakes = {} + for stake_info in stake_infos: + hotkey_ss58 = stake_info.hotkey_ss58 + netuid_ = stake_info.netuid + stake_amount = stake_info.stake + if stake_amount.tao > 0: + hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount + + hotkeys_info = [] + for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): + identity = identities["hotkeys"].get(hotkey_ss58) or old_identities.get( + hotkey_ss58 + ) + hotkey_name = "~" + if identity: + hotkey_name = identity.get("identity", {}).get("name", "") or identity.get( + "display", "~" + ) + # TODO: Add wallet ids here. + + hotkeys_info.append( + { + "index": idx, + "identity": hotkey_name, + "netuids": list(netuid_stakes.keys()), + "hotkey_ss58": hotkey_ss58, + } + ) + + # Display existing hotkeys, id, and staked netuids. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Index", justify="right") + table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + + for hotkey_info in hotkeys_info: + index = str(hotkey_info["index"]) + identity = hotkey_info["identity"] + netuids = group_subnets([n for n in hotkey_info["netuids"]]) + hotkey_ss58 = hotkey_info["hotkey_ss58"] + table.add_row(index, identity, netuids, hotkey_ss58) + + console.print("\n", table) + + # Prompt to select hotkey to unstake. + hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info] + hotkey_idx = Prompt.ask( + "\nEnter the index of the hotkey you want to unstake from", + choices=hotkey_options, + ) + selected_hotkey_info = hotkeys_info[int(hotkey_idx)] + selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"] + selected_hotkey_name = selected_hotkey_info["identity"] + netuid_stakes = hotkey_stakes[selected_hotkey_ss58] + + # Display hotkey's staked netuids with amount. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Subnet", justify="right") + table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column( + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style=COLOR_PALETTE["POOLS"]["RATE"], + justify="left", + ) + + for netuid_, stake_amount in netuid_stakes.items(): + symbol = dynamic_info[netuid_].symbol + rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" + table.add_row(str(netuid_), symbol, str(stake_amount), rate) + console.print("\n", table) + + # Ask which netuids to unstake from for the selected hotkey. + while True: + netuid_input = Prompt.ask( + "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", + default="all", + ) + + if netuid_input.lower() == "all": + selected_netuids = list(netuid_stakes.keys()) + break + else: + try: + netuid_list = [int(n.strip()) for n in netuid_input.split(",")] + invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] + if invalid_netuids: + print_error( + f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + ) + else: + selected_netuids = netuid_list + break + except ValueError: + print_error( + "Please enter valid netuids (numbers), separated by commas, or 'all'." + ) + + hotkeys_to_unstake_from = [] + for netuid_ in selected_netuids: + hotkeys_to_unstake_from.append( + (selected_hotkey_name, selected_hotkey_ss58, netuid_) + ) + return hotkeys_to_unstake_from + + +def ask_unstake_amount( + current_stake_balance: Balance, + netuid: int, + staking_address_name: str, + staking_address_ss58: str, + interactive: bool, +) -> Optional[Balance]: + """Prompt the user to decide the amount to unstake.""" + while True: + response = Prompt.ask( + f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]", + choices=["y", "n", "q"], + default="n" if interactive else "y", + show_choices=True, + ).lower() + + if response == "q": + return None # Quit + + elif response == "y": + return current_stake_balance + + elif response == "n": + while True: + amount_input = Prompt.ask( + f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])" + ) + if amount_input.lower() == "q": + return None # Quit + + try: + amount_value = float(amount_input) + if amount_value <= 0: + console.print("[red]Amount must be greater than zero.[/red]") + continue # Re-prompt + + amount_to_unstake = Balance.from_tao(amount_value) + amount_to_unstake.set_unit(netuid) + if amount_to_unstake > current_stake_balance: + console.print( + f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" + ) + continue # Re-prompt + + return amount_to_unstake + + except ValueError: + console.print( + "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" + ) + + else: + console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]") + + async def unstake( wallet: Wallet, subtensor: "SubtensorInterface", @@ -1170,61 +1365,84 @@ async def unstake( keep_stake: float, unstake_all: bool, prompt: bool, + interactive: bool = False, ): - """Unstake token of amount from hotkey(s).""" - netuids = ( - [int(netuid)] - if netuid is not None - else await subtensor.get_all_subnet_netuids() - ) - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] - if hotkey_ss58_address: - print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") - # Unstake from specific hotkey. - hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] - elif all_hotkeys: - print_verbose("Unstaking from all hotkeys") - # Unstake from all hotkeys. - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - # Exclude hotkeys that are specified. - hotkeys_to_unstake_from = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in exclude_hotkeys - ] - elif include_hotkeys: - print_verbose("Unstaking from included hotkeys") - # Unstake from specific hotkeys. - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - name=wallet.name, - path=wallet.path, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys_to_unstake_from.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) + """Unstake tokens from hotkey(s).""" + with console.status( + f"Retrieving subnet data & identities from {subtensor.network}...", + spinner="earth", + ): + all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.get_all_subnet_dynamic_info(), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + if interactive: + hotkeys_to_unstake_from = await unstake_selection( + subtensor, wallet, all_sn_dynamic_info, ck_hk_identities, old_identities + ) + if not hotkeys_to_unstake_from: + console.print("[red]No unstake operations to perform.[/red]") + return False + netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from}) + else: - # Only cli.config.wallet.hotkey is specified. - # So we unstake from that single hotkey. - print_verbose( - f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" + netuids = ( + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() ) - assert wallet.hotkey is not None - hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] + + # Get the hotkey_names (if any) and the hotkey_ss58s. + hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] + if hotkey_ss58_address: + print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") + # Unstake from specific hotkey. + hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] + elif all_hotkeys: + print_verbose("Unstaking from all hotkeys") + # Unstake from all hotkeys. + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + # Exclude hotkeys that are specified. + hotkeys_to_unstake_from = [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in exclude_hotkeys + ] + elif include_hotkeys: + print_verbose("Unstaking from included hotkeys") + # Unstake from specific hotkeys. + for hotkey_identifier in include_hotkeys: + if is_valid_ss58_address(hotkey_identifier): + # If the hotkey is a valid ss58 address, we add it to the list. + hotkeys_to_unstake_from.append((None, hotkey_identifier)) + else: + # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. + # We then get the hotkey from the wallet and add it to the list. + wallet_ = Wallet( + name=wallet.name, + path=wallet.path, + hotkey=hotkey_identifier, + ) + hotkeys_to_unstake_from.append( + (wallet_.hotkey_str, wallet_.hotkey.ss58_address) + ) + else: + # Only cli.config.wallet.hotkey is specified. + # So we unstake from that single hotkey. + print_verbose( + f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" + ) + assert wallet.hotkey is not None + hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] with console.status( - f"Retrieving stake and subnet data from {subtensor.network}...", + f"Retrieving stake data from {subtensor.network}...", spinner="earth", ): - # Prepare lists to store unstaking data per subnet + # Prepare unstaking transactions unstake_operations = [] total_received_amount = Balance.from_tao(0) current_wallet_balance: Balance = ( @@ -1232,42 +1450,49 @@ async def unstake( )[wallet.coldkeypub.ss58_address] max_float_slippage = 0 - # Fetch dynamic info and stake balances + # Fetch stake balances chain_head = await subtensor.substrate.get_chain_head() - dynamic_info_list, stake_all_netuids = await asyncio.gather( - asyncio.gather( - *[subtensor.get_subnet_dynamic_info(x, chain_head) for x in netuids] - ), - subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( + stake_in_netuids = ( + await subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( hotkey_ss58s=[hk[1] for hk in hotkeys_to_unstake_from], coldkey_ss58=wallet.coldkeypub.ss58_address, netuids=netuids, block_hash=chain_head, - ), + ) + ) + + # Flag to check if user wants to quit + skip_remaining_subnets = False + if hotkeys_to_unstake_from: + console.print( + "[dark_sea_green3]Tip: Enter 'q' any time to skip further entries and process existing unstakes" ) - dynamic_info_all_netuids = dict(zip(netuids, dynamic_info_list)) # Iterate over hotkeys and netuids to collect unstake operations for hotkey in hotkeys_to_unstake_from: - staking_address_name, staking_address_ss58 = hotkey + if skip_remaining_subnets: + break + + if interactive: + staking_address_name, staking_address_ss58, netuid = hotkey + netuids_to_process = [netuid] + else: + staking_address_name, staking_address_ss58 = hotkey + netuids_to_process = netuids + initial_amount = amount - skip_remaining_subnets = False # Flag to check if user wants to quit - if len(netuids) > 1: + if len(netuids_to_process) > 1: console.print( "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" ) - for netuid in netuids: + for netuid in netuids_to_process: if skip_remaining_subnets: break # Exit the loop over netuids - dynamic_info = dynamic_info_all_netuids.get(netuid) - if dynamic_info is None: - console.print(f"[red]Subnet: {netuid} does not exist.[/red]") - continue # Skip to the next subnet - - current_stake_balance = stake_all_netuids[staking_address_ss58][netuid] + dynamic_info = all_sn_dynamic_info.get(netuid) + current_stake_balance = stake_in_netuids[staking_address_ss58][netuid] if current_stake_balance.tao == 0: continue # No stake to unstake @@ -1277,70 +1502,18 @@ async def unstake( elif unstake_all: amount_to_unstake_as_balance = current_stake_balance else: - # Prompt the user for each subnet - while True: - response = Prompt.ask( - f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]", - choices=["y", "n", "q"], - default="n", - show_choices=True, - ).lower() - - if response.lower() == "q": - skip_remaining_subnets = True - break # Exit the loop over netuids - - elif response.lower() == "y": - amount_to_unstake_as_balance = current_stake_balance - break # Proceed with unstake operation - elif response.lower() == "n": - while True: - amount_input = Prompt.ask( - f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])" - ) - if amount_input.lower() == "q": - skip_remaining_subnets = True - break # Exit the loop over netuids - - try: - amount_value = float(amount_input) - if amount_value < 0 or amount_value == 0: - console.print( - "[red]Amount cannot be negative or zero.[/red]" - ) - continue # Re-prompt - - amount_to_unstake_as_balance = Balance.from_tao( - amount_value - ) - amount_to_unstake_as_balance.set_unit(netuid) - if amount_to_unstake_as_balance > current_stake_balance: - console.print( - f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" - ) - continue # Re-prompt - - break # Valid amount entered - - except ValueError: - console.print( - "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" - ) - - if skip_remaining_subnets: - break # Exit the loop over netuids - - break # Exit the response loop - - else: - console.print( - "[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]" - ) - continue # Re-prompt - - if skip_remaining_subnets: - break # Exit the loop over netuids + amount_to_unstake_as_balance = ask_unstake_amount( + current_stake_balance, + netuid, + staking_address_name + if staking_address_name + else staking_address_ss58, + staking_address_ss58, + interactive, + ) + if amount_to_unstake_as_balance is None: + skip_remaining_subnets = True + break # Check enough stake to remove. amount_to_unstake_as_balance.set_unit(netuid) @@ -1349,7 +1522,7 @@ async def unstake( f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]" f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]" ) - continue # Skip to the next subnet + continue # Skip to the next subnet - useful when single amount is specified for all subnets received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( amount_to_unstake_as_balance @@ -1389,7 +1562,9 @@ async def unstake( # Build the table table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]," + f" Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1441,7 +1616,8 @@ async def unstake( console.print( "\n" f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," + " this may result in a loss of funds.\n" f"-------------------------------------------------------------------------------------------------------------------\n" ) @@ -1451,7 +1627,7 @@ async def unstake( The table displays information about the stake remove operation you are about to perform. The columns are as follows: - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. - - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are unstaking from. + - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. - [bold white]Amount[/bold white]: The stake amount you are removing from this key. - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. @@ -1469,7 +1645,7 @@ async def unstake( err_console.print("Error decrypting coldkey (possibly incorrect password)") return False - with console.status(f"\n:satellite: Performing unstaking operations...") as status: + with console.status("\n:satellite: Performing unstaking operations...") as status: for op in unstake_operations: netuid_i = op["netuid"] staking_address_name = op["hotkey_name"] @@ -1522,7 +1698,8 @@ async def unstake( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." @@ -2060,3 +2237,10 @@ async def move_stake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) return + + +async def fetch_coldkey_stake(subtensor: "SubtensorInterface", wallet: Wallet): + sub_stakes = await subtensor.get_stake_info_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + return sub_stakes diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 09f1d3f2..7fa2e40c 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -580,7 +580,7 @@ async def show_root(): return table = Table( - title=f"[underline dark_orange]Root Network[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}[/dark_orange]\n", + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Root Network\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -593,27 +593,27 @@ async def show_root(): table.add_column("[bold white]Position", style="white", justify="center") table.add_column( f"[bold white] TAO ({Balance.get_unit(0)})", - style="medium_purple", + style=COLOR_PALETTE["STAKE"]["TAO"], justify="center", ) table.add_column( f"[bold white]Stake ({Balance.get_unit(0)})", - style="rgb(42,161,152)", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="center", ) table.add_column( f"[bold white]Emission ({Balance.get_unit(0)}/block)", - style="tan", + style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="center", ) table.add_column( "[bold white]Hotkey", - style="plum2", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], justify="center", ) table.add_column( "[bold white]Coldkey", - style="plum2", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], justify="center", ) From a382b757b6b477efbcc74dc1e0b669cd7cc4884f Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Thu, 21 Nov 2024 14:21:04 -0800 Subject: [PATCH 141/332] Modifying descriptions and links in stake and subnets dot py files --- bittensor_cli/src/commands/stake/stake.py | 18 +++++++++--------- bittensor_cli/src/commands/subnets.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b5f1c01e..38fec1be 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1946,35 +1946,35 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]Stake (α)[/bold tan]", - "Stake this hotkey holds in the subnet, expressed in subnet's dynamic TAO currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#staking[/blue].", ), ( "[bold tan]TAO Reserves (τ_in)[/bold tan]", - 'Units of TAO in the TAO Reserves reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Reserves (α_in)" description. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', ), ( "[bold tan]Alpha Reserves (α_in)[/bold tan]", - 'Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with "TAO Reserves(τ_in)", form the subnet pool for every subnet. This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Reserves(τ_in) / Alpha Reserves (α_in)). This can change every block when staking or unstaking or emissions occur on this subnet. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", ), ( "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", ), ( "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', ), ( "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", ), ( "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", ), ( "[bold tan]Registered[/bold tan]", @@ -1982,7 +1982,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): ), ( "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", ), ] diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 7fa2e40c..0d31ce08 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -521,31 +521,31 @@ def format_cell(value, previous_value, unit="", precision=4): ), ( "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ/block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's TAO Pool (τ_in) by the sum of all TAO Pool (τ_in) across all the subnets. This fraction is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Shows how the one τ per block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's alpha token price by the sum of all alpha prices across all the subnets. This fraction of TAO is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", ), ( "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's dynamic TAO currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", ), ( - "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Units of TAO in the TAO pool reserves for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', ), ( - "[bold tan]Alpha Pool (α_in)[/bold tan]", - "Units of subnet dTAO token in the dTAO pool reserves for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as (TAO Pool (τ_in) / Alpha Pool (α_in)). This can change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + 'Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].', ), ( "[bold tan]Tempo (k/n)[/bold tan]", - 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].', + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#tempo-kn[/blue].', ), ( "[bold tan]Local weight coeff (γ)[/bold tan]", - "A multiplication factor between 0 and 1, applied to the relative proportion of a validator's stake in this subnet. Applied as (γ) × (a validator's α stake in this subnet) / (Total α stake in this subnet, i.e., Stake (α_out)). This γ-weighted relative proportion is used, in addition to other factors, in determining the overall stake weight of a validator in this subnet. This is a subnet parameter. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + "This is the global_split coefficient. It is a multiplication factor between 0 and 1, and it controls the balance between a validator's normalized global and local weights. In effect, the global_split parameter controls the balance between the validator hotkey's local and global influence. This is a subnet parameter. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#global-split[/blue].", ), ] From e7d12c877b326241ba721ac7d990634a6ed7d275 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 18 Dec 2024 12:24:29 -0800 Subject: [PATCH 142/332] Bumps websocket to 13.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e891bcf..25066875 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ rich~=13.7 scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 -websockets>=12.0 +websockets==13.0 bittensor-wallet>=2.0.2 bt-decode==0.2.0a0 \ No newline at end of file From 4c555333a82de99038cbbe0e60d607ae18d8ff01 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 18 Dec 2024 15:47:52 -0800 Subject: [PATCH 143/332] lock_cost -> burn_cost --- bittensor_cli/cli.py | 14 +++++++------- bittensor_cli/src/commands/subnets.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f7ae8f20..69f3b8c5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -725,8 +725,8 @@ def __init__(self): "list", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_list) self.subnets_app.command( - "lock-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] - )(self.subnets_lock_cost) + "burn-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] + )(self.subnets_burn_cost) self.subnets_app.command( "create", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] )(self.subnets_create) @@ -787,7 +787,7 @@ def __init__(self): )(self.wallet_get_id) # Subnets - self.subnets_app.command("lock_cost", hidden=True)(self.subnets_lock_cost) + self.subnets_app.command("burn_cost", hidden=True)(self.subnets_burn_cost) self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) # Sudo @@ -3480,23 +3480,23 @@ def subnets_show( ) ) - def subnets_lock_cost( + def subnets_burn_cost( self, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Shows the required amount of TAO to be locked for creating a new subnet, i.e., cost of registering a new subnet. + Shows the required amount of TAO to be recycled for creating a new subnet, i.e., cost of registering a new subnet. The current implementation anneals the cost of creating a subnet over a period of two days. If the displayed cost is unappealing to you, check back in a day or two to see if it has decreased to a more affordable level. EXAMPLE - [green]$[/green] btcli subnets lock_cost + [green]$[/green] btcli subnets burn_cost """ self.verbosity_handler(quiet, verbose) - return self._run_command(subnets.lock_cost(self.initialize_chain(network))) + return self._run_command(subnets.burn_cost(self.initialize_chain(network))) def subnets_create( self, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 0d31ce08..5c1f3818 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -94,8 +94,8 @@ async def _find_event_attributes_in_extrinsic_receipt( your_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) your_balance = your_balance_[wallet.coldkeypub.ss58_address] - print_verbose("Fetching lock_cost") - burn_cost = await lock_cost(subtensor) + print_verbose("Fetching burn_cost") + burn_cost = await burn_cost(subtensor) if burn_cost > your_balance: err_console.print( f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}] is not enough to pay the subnet lock cost of: " @@ -824,7 +824,7 @@ async def show_subnet(netuid_: int): await show_subnet(netuid) -async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: +async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( f":satellite:Retrieving lock cost from {subtensor.network}...", @@ -836,13 +836,13 @@ async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: params=[], ) if lc: - lock_cost_ = Balance(lc) + burn_cost_ = Balance(lc) console.print( - f"Subnet lock cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{lock_cost_}" + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost_}" ) - return lock_cost_ + return burn_cost_ else: - err_console.print("Subnet lock cost: [red]Failed to get subnet lock cost[/red]") + err_console.print("Subnet burn cost: [red]Failed to get subnet burn cost[/red]") return None From d0bc140233328e8953fb40b5e0731d4eb12fa068 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 19 Dec 2024 09:39:38 -0800 Subject: [PATCH 144/332] Various improvements and changes --- bittensor_cli/cli.py | 3 +- bittensor_cli/src/__init__.py | 155 ++++++++++++----- bittensor_cli/src/commands/stake/stake.py | 6 +- bittensor_cli/src/commands/subnets.py | 195 ++++++++++++++-------- 4 files changed, 242 insertions(+), 117 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 69f3b8c5..6d112c5f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -598,7 +598,7 @@ def __init__(self): ) # utils app - self.app.add_typer(self.utils_app, name="utils", no_args_is_help=True) + self.app.add_typer(self.utils_app, name="utils", no_args_is_help=True, hidden=True) # config commands self.config_app.command("set")(self.set_config) @@ -3476,6 +3476,7 @@ def subnets_show( subnets.show( subtensor, netuid, + verbose=verbose, prompt=prompt, ) ) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index b3077d8e..b44f65ce 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -553,59 +553,122 @@ class WalletValidationTypes(Enum): COLOR_PALETTE = { "GENERAL": { - "HEADER": "#4196D6", - "LINKS": "#8CB9E9", - "HINT": "#A2E5B8", - "COLDKEY": "#9EF5E4", - "HOTKEY": "#ECC39D", - "SUBHEADING_MAIN": "#7ECFEC", - "SUBHEADING": "#AFEFFF", - "SUBHEADING_EXTRA_1": "#96A3C5", - "SUBHEADING_EXTRA_2": "#6D7BAF", - "CONFIRMATION_Y_N_Q": "#EE8DF8", - "SYMBOL": "#E7CC51", - "BALANCE": "#4F91C6", - "COST": "#53B5A0", - "SUCCESS": "#53B5A0", - "NETUID": "#CBA880", - "NETUID_EXTRA": "#DDD5A9", - "TEMPO": "#67A3A5", + "HEADER": "#4196D6", # Light Blue + "LINKS": "#8CB9E9", # Sky Blue + "HINT": "#A2E5B8", # Mint Green + "COLDKEY": "#9EF5E4", # Aqua + "HOTKEY": "#ECC39D", # Light Orange/Peach + "SUBHEADING_MAIN": "#7ECFEC", # Light Cyan + "SUBHEADING": "#AFEFFF", # Pale Blue + "SUBHEADING_EXTRA_1": "#96A3C5", # Grayish Blue + "SUBHEADING_EXTRA_2": "#6D7BAF", # Slate Blue + "CONFIRMATION_Y_N_Q": "#EE8DF8", # Light Purple/Pink + "SYMBOL": "#E7CC51", # Gold + "BALANCE": "#4F91C6", # Medium Blue + "COST": "#53B5A0", # Teal + "SUCCESS": "#53B5A0", # Teal + "NETUID": "#CBA880", # Tan + "NETUID_EXTRA": "#DDD5A9", # Light Khaki + "TEMPO": "#67A3A5", # Grayish Teal }, "STAKE": { - "STAKE_AMOUNT": "#53B5A0", - "STAKE_ALPHA": "#53B5A0", - "STAKE_SWAP": "#67A3A5", - "TAO": "#4F91C6", - "SLIPPAGE_TEXT": "#C25E7C", - "SLIPPAGE_PERCENT": "#E7B195", - "NOT_REGISTERED": "#EB6A6C", - "EXTRA_1": "#D781BB", + "STAKE_AMOUNT": "#53B5A0", # Teal + "STAKE_ALPHA": "#53B5A0", # Teal + "STAKE_SWAP": "#67A3A5", # Grayish Teal + "TAO": "#4F91C6", # Medium Blue + "SLIPPAGE_TEXT": "#C25E7C", # Rose + "SLIPPAGE_PERCENT": "#E7B195", # Light Coral + "NOT_REGISTERED": "#EB6A6C", # Salmon Red + "EXTRA_1": "#D781BB", # Pink }, "POOLS": { - "TAO": "#4F91C6", - "ALPHA_IN": "#D09FE9", - "ALPHA_OUT": "#AB7CC8", - "RATE": "#F8D384", - "TAO_EQUIV": "#8CB9E9", - "EMISSION": "#F8D384", - "EXTRA_1": "#CAA8FB", - "EXTRA_2": "#806DAF", + "TAO": "#4F91C6", # Medium Blue + "ALPHA_IN": "#D09FE9", # Light Purple + "ALPHA_OUT": "#AB7CC8", # Medium Purple + "RATE": "#F8D384", # Light Orange + "TAO_EQUIV": "#8CB9E9", # Sky Blue + "EMISSION": "#F8D384", # Light Orange + "EXTRA_1": "#CAA8FB", # Lavender + "EXTRA_2": "#806DAF", # Dark Purple }, "GREY": { - "GREY_100": "#F8F9FA", - "GREY_200": "#F1F3F4", - "GREY_300": "#DBDDE1", - "GREY_400": "#BDC1C6", - "GREY_500": "#5F6368", - "GREY_600": "#2E3134", - "GREY_700": "#282A2D", - "GREY_800": "#17181B", - "GREY_900": "#0E1013", - "BLACK": "#000000", + "GREY_100": "#F8F9FA", # Almost White + "GREY_200": "#F1F3F4", # Very Light Grey + "GREY_300": "#DBDDE1", # Light Grey + "GREY_400": "#BDC1C6", # Medium Light Grey + "GREY_500": "#5F6368", # Medium Grey + "GREY_600": "#2E3134", # Medium Dark Grey + "GREY_700": "#282A2D", # Dark Grey + "GREY_800": "#17181B", # Very Dark Grey + "GREY_900": "#0E1013", # Almost Black + "BLACK": "#000000", # Pure Black }, "SUDO": { - "HYPERPARAMETER": "#4F91C6", - "VALUE": "#D09FE9", - "NORMALIZED": "#AB7CC8", + "HYPERPARAMETER": "#4F91C6", # Medium Blue + "VALUE": "#D09FE9", # Light Purple + "NORMALIZED": "#AB7CC8", # Medium Purple }, } + + +SUBNETS = { + 0: "root", + 1: "apex", + 2: "omron", + 3: "templar", + 4: "targon", + 5: "kaito", + 6: "infinite", + 7: "subvortex", + 8: "rpn", + 9: "pretrain", + 10: "sturday", + 11: "dippy", + 12: "horde", + 13: "dataverse", + 14: "palaidn", + 15: "deval", + 16: "bitrads", + 17: "3gen", + 18: "cortex", + 19: "inference", + 20: "bitagent", + 21: "any-any", + 22: "meta", + 23: "social", + 24: "omega", + 25: "protein", + 26: "alchemy", + 27: "compute", + 28: "oracle", + 29: "coldint", + 30: "bet", + 31: "naschain", + 32: "itsai", + 33: "ready", + 34: "mind", + 35: "logic", + 36: "automata", + 37: "tuning", + 38: "distributed", + 39: "edge", + 40: "chunk", + 41: "sportsensor", + 42: "masa", + 43: "graphite", + 44: "score", + 45: "gen42", + 46: "neural", + 47: "condense", + 48: "nextplace", + 49: "automl", + 50: "audio", + 51: "celium", + 52: "dojo", + 53: "frontier", + 54: "docs-insight", + 56: "gradients", + 57: "gaia", + 58: "dippy-speech", + 59: "agent-arena" +} diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 38fec1be..7dabba6c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1799,8 +1799,10 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered str(Balance.from_tao(per_block_emission).set_unit(netuid)) - if substake_.is_registered - else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) + # Removing this flag for now, TODO: Confirm correct values are here w.r.t CHKs + + # if substake_.is_registered + # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) ] ) # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 5c1f3818..5d6582b5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -14,7 +14,7 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE, SUBNETS from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( @@ -181,7 +181,7 @@ async def fetch_subnet_data(): ) return subnets, global_weights, identities - def define_table(total_emissions: float, total_rate: float): + def define_table(total_emissions: float, total_rate: float, total_netuids: int): table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", @@ -195,13 +195,13 @@ def define_table(total_emissions: float, total_rate: float): pad_edge=True, ) - table.add_column("[bold white]Netuid", style="grey89", justify="center") + table.add_column("[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids)) table.add_column( "[bold white]Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="right", ) - table.add_column("[bold white]Owner", style="cyan", justify="left") + table.add_column("[bold white]Name", style="cyan", justify="left") table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], @@ -209,9 +209,10 @@ def define_table(total_emissions: float, total_rate: float): footer=f"τ {total_emissions:.4f}", ) table.add_column( - f"[bold white]Stake ({Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style="#AB7CC8", justify="left", + footer=f"τ {total_rate:.4f}", ) table.add_column( f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", @@ -224,10 +225,9 @@ def define_table(total_emissions: float, total_rate: float): justify="left", ) table.add_column( - f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", - style=COLOR_PALETTE["POOLS"]["RATE"], + f"[bold white]Stake ({Balance.get_unit(1)}_out)", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", - footer=f"τ {total_rate:.4f}", ) table.add_column( "[bold white]Tempo (k/n)", @@ -258,12 +258,12 @@ def create_table(subnets, global_weights, identities): # Prepare cells netuid_cell = str(netuid) symbol_cell = f"{subnet.symbol}" - identity_cell = identity + subnet_name_cell = SUBNETS.get(netuid, "~") emission_cell = f"{emission_tao:,.4f}" - alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" + price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" tao_in_cell = f"{subnet.tao_in.tao:,.4f} τ" alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" - price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" + alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" global_weight_cell = ( f"{global_weight:.4f}" if global_weight is not None else "N/A" @@ -271,16 +271,16 @@ def create_table(subnets, global_weights, identities): rows.append( ( - netuid_cell, # Netuid - symbol_cell, # Symbol - identity_cell, # Identity - emission_cell, # Emission (τ) - alpha_out_cell, # Stake α_out - tao_in_cell, # TAO Pool τ_in - alpha_in_cell, # Alpha Pool α_in - price_cell, # Rate τ_in/α_in - tempo_cell, # Tempo k/n - global_weight_cell, # Local weight coeff. (γ) + netuid_cell, # Netuid + symbol_cell, # Symbol + subnet_name_cell, # Name + emission_cell, # Emission (τ) + price_cell, # Rate τ_in/α_in + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + alpha_out_cell, # Stake α_out + tempo_cell, # Tempo k/n + global_weight_cell,# Local weight coeff. (γ) ) ) @@ -290,11 +290,12 @@ def create_table(subnets, global_weights, identities): total_rate = sum( float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) - table = define_table(total_emissions, total_rate) + total_netuids = len(subnets) + table = define_table(total_emissions, total_rate, total_netuids) - # Sort rows by emission, keeping the root subnet in the first position + # Sort rows by stake, keeping the root subnet in the first position sorted_rows = [rows[0]] + sorted( - rows[1:], key=lambda x: float(str(x[3]).replace(",", "")), reverse=True + rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True ) for row in sorted_rows: @@ -349,16 +350,13 @@ def format_cell(value, previous_value, unit="", precision=4): # Prepare cells netuid_cell = str(netuid) - identity_cell = identity symbol_cell = f"{subnet.symbol}" + subnet_name_cell = SUBNETS.get(netuid, "~") emission_cell = format_cell( emission_tao, prev.get("emission_tao"), unit="", precision=4 ) - alpha_out_cell = format_cell( - subnet.alpha_out.tao, - prev.get("alpha_out"), - unit=f" {symbol}", - precision=5, + price_cell = format_cell( + subnet.price.tao, prev.get("price"), unit=f" τ/{symbol}", precision=4 ) tao_in_cell = format_cell( subnet.tao_in.tao, prev.get("tao_in"), unit=" τ", precision=4 @@ -369,8 +367,11 @@ def format_cell(value, previous_value, unit="", precision=4): unit=f" {symbol}", precision=4, ) - price_cell = format_cell( - subnet.price.tao, prev.get("price"), unit=f" τ/{symbol}", precision=4 + alpha_out_cell = format_cell( + subnet.alpha_out.tao, + prev.get("alpha_out"), + unit=f" {symbol}", + precision=5, ) # Tempo cell @@ -422,16 +423,16 @@ def format_cell(value, previous_value, unit="", precision=4): rows.append( ( - netuid_cell, # Netuid - symbol_cell, # Symbol - identity_cell, # Identity - emission_cell, # Emission (τ) - alpha_out_cell, # Stake α_out - tao_in_cell, # TAO Pool τ_in - alpha_in_cell, # Alpha Pool α_in - price_cell, # Rate τ_in/α_in - tempo_cell, # Tempo k/n - global_weight_cell, # Local weight coeff. (γ) + netuid_cell, # Netuid + symbol_cell, # Symbol + subnet_name_cell, # Name + emission_cell, # Emission (τ) + price_cell, # Rate τ_in/α_in + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + alpha_out_cell, # Stake α_out + tempo_cell, # Tempo k/n + global_weight_cell,# Local weight coeff. (γ) ) ) @@ -441,13 +442,12 @@ def format_cell(value, previous_value, unit="", precision=4): total_rate = sum( float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) - table = define_table(total_emissions, total_rate) + total_netuids = len(subnets) + table = define_table(total_emissions, total_rate, total_netuids) - # Sort rows by emission, keeping the first subnet in the first position + # Sort rows by stake, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted( - rows[1:], - key=lambda x: float(str(x[3]).split()[0].replace(",", "")), - reverse=True, + rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True ) for row in sorted_rows: table.add_row(*row) @@ -466,21 +466,43 @@ def format_cell(value, previous_value, unit="", precision=4): ) progress_task = progress.add_task("Updating:", total=refresh_interval) + previous_block = None + current_block = None previous_data = None + with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, global_weights, identities = await fetch_subnet_data() + subnets = await subtensor.get_all_subnet_dynamic_info() + global_weights, identities, block_number = await asyncio.gather( + subtensor.get_global_weights([subnet.netuid for subnet in subnets]), + subtensor.query_all_identities(), + subtensor.substrate.get_block_number(None) + ) + + # Update block numbers + previous_block = current_block + current_block = block_number + new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) + table, current_data = create_table_live( subnets, global_weights, identities, previous_data ) previous_data = current_data progress.reset(progress_task) start_time = asyncio.get_event_loop().time() - message = "\nLive view active. Press [bold red]Ctrl + C[/bold red] to exit" - live_render = Group(table, progress, message) + block_info = ( + f"Previous: [dark_sea_green]{previous_block if previous_block else 'N/A'}[/dark_sea_green] " + f"Current: [dark_sea_green]{current_block}[/dark_sea_green] " + f"New: [dark_sea_green]{new_blocks}[/dark_sea_green] " + ) + + message = f"Live view active. Press [bold red]Ctrl + C[/bold red] to exit\n{block_info}" + + live_render = Group(message, progress, table) live.update(live_render) + while not progress.finished: await asyncio.sleep(0.1) elapsed = asyncio.get_event_loop().time() - start_time @@ -556,15 +578,20 @@ def format_cell(value, previous_value, unit="", precision=4): console.print(description_table) -async def show(subtensor: "SubtensorInterface", netuid: int, prompt: bool = True): +async def show(subtensor: "SubtensorInterface", netuid: int, verbose: bool = False, prompt: bool = True): async def show_root(): all_subnets = await subtensor.get_all_subnet_dynamic_info() - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_state", - params=[0], + hex_bytes_result, identities, old_identities = await asyncio.gather( + subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[0], + ), + subtensor.query_all_identities(), + subtensor.get_delegate_identities(), ) + if (bytes_result := hex_bytes_result) is None: err_console.print("The root subnet does not exist") return @@ -616,6 +643,11 @@ async def show_root(): style=COLOR_PALETTE["GENERAL"]["COLDKEY"], justify="center", ) + table.add_column( + "[bold white]Identity", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="left", + ) sorted_hotkeys = sorted( enumerate(root_state.hotkeys), @@ -632,13 +664,20 @@ async def show_root(): total_emission_per_block += subnet.alpha_to_tao( Balance.from_rao(emission_on_subnet) ) + + # Get identity for this validator + coldkey_identity = identities.get(root_state.coldkeys[idx], {}).get("name", "") + hotkey_identity = old_identities.get(root_state.hotkeys[idx]) + validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") + table.add_row( str((pos + 1)), str(root_state.global_stake[idx]), str(root_state.local_stake[idx]), f"{(total_emission_per_block)}", - f"{root_state.hotkeys[idx]}", - f"{root_state.coldkeys[idx]}", + f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", + f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", + validator_identity, ) # Print the table @@ -658,12 +697,19 @@ async def show_root(): ) async def show_subnet(netuid_: int): - subnet_info = await subtensor.get_subnet_dynamic_info(netuid_) - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_state", - params=[netuid_], + subnet_info, hex_bytes_result, identities, old_identities = await asyncio.gather( + subtensor.get_subnet_dynamic_info(netuid_), + subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid_], + ), + subtensor.query_all_identities(), + subtensor.get_delegate_identities(), ) + owner_ss58 = subnet_info.owner if subnet_info else "" + owner_identity = identities.get(owner_ss58, {}).get("name", old_identities.get(owner_ss58).display if old_identities.get(owner_ss58) else "") + if (bytes_result := hex_bytes_result) is None: err_console.print(f"Subnet {netuid_} does not exist") return @@ -683,7 +729,8 @@ async def show_subnet(netuid_: int): # Define table properties table = Table( - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" + f"{': ' + SUBNETS.get(netuid_, '') if SUBNETS.get(netuid_) else ''}" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, @@ -715,6 +762,12 @@ async def show_subnet(netuid_: int): tao_sum += subnet_state.global_stake[idx] stake_sum += subnet_state.local_stake[idx] stake_weight_sum += subnet_state.stake_weight[idx] + + # Get identity for this uid + coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get("name", "") + hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) + uid_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "~") + rows.append( ( str(idx), # UID @@ -726,8 +779,9 @@ async def show_subnet(netuid_: int): str(subnet_state.incentives[idx]), # Incentive # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions - f"{subnet_state.hotkeys[idx]}", # Hotkey - f"{subnet_state.coldkeys[idx]}", # Coldkey + f"{subnet_state.hotkeys[idx][:6]}" if not verbose else f"{subnet_state.hotkeys[idx]}", # Hotkey + f"{subnet_state.coldkeys[idx][:6]}" if not verbose else f"{subnet_state.coldkeys[idx]}", # Coldkey + uid_identity, # Identity ) ) # Add columns to the table @@ -789,6 +843,12 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="center", ) + table.add_column( + "Identity", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + no_wrap=True, + justify="left", + ) for row in rows: table.add_row(*row) @@ -797,8 +857,7 @@ async def show_subnet(netuid_: int): console.print(table) console.print("\n") console.print( - f"Subnet: {netuid_}:\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" Total Locked: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{subnet_info.total_locked}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f"Subnet: {netuid_}:\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" f" Owner Locked: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{subnet_info.owner_locked}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) console.print( From 1f70cf5b421f165568b4dff6088fa6f4e24756a9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 19 Dec 2024 17:01:42 -0800 Subject: [PATCH 145/332] s list changes --- bittensor_cli/src/commands/subnets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 5d6582b5..eb0cee47 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -202,18 +202,18 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): justify="right", ) table.add_column("[bold white]Name", style="cyan", justify="left") - table.add_column( - f"[bold white]Emission ({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["EMISSION"], - justify="left", - footer=f"τ {total_emissions:.4f}", - ) table.add_column( f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="#AB7CC8", justify="left", footer=f"τ {total_rate:.4f}", ) + table.add_column( + f"[bold white]Emission ({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="left", + footer=f"τ {total_emissions:.4f}", + ) table.add_column( f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], @@ -274,8 +274,8 @@ def create_table(subnets, global_weights, identities): netuid_cell, # Netuid symbol_cell, # Symbol subnet_name_cell, # Name - emission_cell, # Emission (τ) price_cell, # Rate τ_in/α_in + emission_cell, # Emission (τ) tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out @@ -426,8 +426,8 @@ def format_cell(value, previous_value, unit="", precision=4): netuid_cell, # Netuid symbol_cell, # Symbol subnet_name_cell, # Name - emission_cell, # Emission (τ) price_cell, # Rate τ_in/α_in + emission_cell, # Emission (τ) tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out @@ -495,7 +495,7 @@ def format_cell(value, previous_value, unit="", precision=4): block_info = ( f"Previous: [dark_sea_green]{previous_block if previous_block else 'N/A'}[/dark_sea_green] " f"Current: [dark_sea_green]{current_block}[/dark_sea_green] " - f"New: [dark_sea_green]{new_blocks}[/dark_sea_green] " + f"Diff: [dark_sea_green]{new_blocks}[/dark_sea_green] " ) message = f"Live view active. Press [bold red]Ctrl + C[/bold red] to exit\n{block_info}" From b352dd735772a4df998756e737908ed2e46f5142 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 19 Dec 2024 18:08:23 -0800 Subject: [PATCH 146/332] s show changes --- bittensor_cli/src/commands/subnets.py | 40 ++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index eb0cee47..a21f07d4 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -581,6 +581,7 @@ def format_cell(value, previous_value, unit="", precision=4): async def show(subtensor: "SubtensorInterface", netuid: int, verbose: bool = False, prompt: bool = True): async def show_root(): all_subnets = await subtensor.get_all_subnet_dynamic_info() + root_info = all_subnets[0] hex_bytes_result, identities, old_identities = await asyncio.gather( subtensor.query_runtime_api( @@ -682,6 +683,17 @@ async def show_root(): # Print the table console.print(table) + console.print("\n") + + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]0 τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.tao_in.tao:,.4f} τ[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.alpha_in.tao:,.4f} {root_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.alpha_out.tao:,.5f} {root_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + ) console.print( """ Description: @@ -752,6 +764,7 @@ async def show_subnet(netuid_: int): stake_sum = Balance(0) relative_emissions_sum = 0 stake_weight_sum = 0 + for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum @@ -784,7 +797,15 @@ async def show_subnet(netuid_: int): uid_identity, # Identity ) ) - # Add columns to the table + + # Sort rows by stake + sorted_rows = sorted( + rows, + key=lambda x: float(str(x[2]).split()[0].replace(",", "")), + reverse=True + ) + + # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( f"TAO({Balance.get_unit(0)})", @@ -849,16 +870,26 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="left", ) - for row in rows: + for row in sorted_rows: table.add_row(*row) # Print the table console.print("\n\n") console.print(table) console.print("\n") + + subnet_name = SUBNETS.get(netuid_, '') + subnet_name_display = f": {subnet_name}" if subnet_name else "" + console.print( - f"Subnet: {netuid_}:\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" Owner Locked: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{subnet_info.owner_locked}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.emission.tao:,.4f} τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.tao_in.tao:,.4f} τ[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) console.print( """ @@ -877,6 +908,7 @@ async def show_subnet(netuid_: int): """ ) + if netuid == 0: await show_root() else: From 60ba785ee52c71d6374d85e729d0a39a9689c8f9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 20 Dec 2024 11:36:26 -0800 Subject: [PATCH 147/332] Enhancements: wallet, stake, s show --- bittensor_cli/cli.py | 57 ++++++++++++++----- .../src/bittensor/subtensor_interface.py | 4 +- bittensor_cli/src/commands/stake/stake.py | 24 ++------ bittensor_cli/src/commands/subnets.py | 8 +-- bittensor_cli/src/commands/wallets.py | 5 +- 5 files changed, 57 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6d112c5f..d698039c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2116,7 +2116,7 @@ def wallet_balance( """ self.verbosity_handler(quiet, verbose) - + wallet = None if ss58_addresses: valid_ss58s = [ ss58 for ss58 in set(ss58_addresses) if is_valid_ss58_address(ss58) @@ -2127,20 +2127,44 @@ def wallet_balance( print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") if valid_ss58s: - wallet = None ss58_addresses = valid_ss58s else: raise typer.Exit() else: - ask_for = [WO.PATH] if all_balances else [WO.NAME, WO.PATH] - validate = WV.NONE if all_balances else WV.WALLET - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=ask_for, - validate=validate, - ) + if wallet_name: + coldkey_or_ss58 = wallet_name + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 addresses[/blue] (comma-separated)", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + # Split by comma and strip whitespace + coldkey_or_ss58_list = [x.strip() for x in coldkey_or_ss58.split(",")] + + # Check if any entry is a valid SS58 address + if any(is_valid_ss58_address(x) for x in coldkey_or_ss58_list): + valid_ss58s = [ + ss58 for ss58 in coldkey_or_ss58_list if is_valid_ss58_address(ss58) + ] + invalid_ss58s = set(coldkey_or_ss58_list) - set(valid_ss58s) + for invalid_ss58 in invalid_ss58s: + print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") + + if valid_ss58s: + ss58_addresses = valid_ss58s + else: + raise typer.Exit() + else: + wallet_name = coldkey_or_ss58_list[0] if coldkey_or_ss58_list else wallet_name + ask_for = [WO.PATH] if all_balances else [WO.NAME, WO.PATH] + validate = WV.NONE if all_balances else WV.WALLET + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=validate, + ) subtensor = self.initialize_chain(network) return self._run_command( wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) @@ -2430,10 +2454,13 @@ def stake_list( print_error("You entered an invalid ss58 address") raise typer.Exit() else: - coldkey_or_ss58 = Prompt.ask( - "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", - default=self.config.get("wallet_name") or defaults.wallet.name, - ) + if wallet_name: + coldkey_or_ss58 = wallet_name + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) if is_valid_ss58_address(coldkey_or_ss58): coldkey_ss58 = coldkey_or_ss58 else: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 87f3c250..7bd01d42 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -901,6 +901,9 @@ async def fetch_coldkey_hotkey_identities( """ coldkey_identities = await self.query_all_identities() + identities = {"coldkeys": {}, "hotkeys": {}} + if not coldkey_identities: + return identities query = await self.substrate.query_multiple( params=[(ss58) for ss58, _ in coldkey_identities.items()], module="SubtensorModule", @@ -909,7 +912,6 @@ async def fetch_coldkey_hotkey_identities( reuse_block_hash=reuse_block, ) - identities = {"coldkeys": {}, "hotkeys": {}} for coldkey_ss58, hotkeys in query.items(): coldkey_identity = coldkey_identities.get(coldkey_ss58) hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []] diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 7dabba6c..cb1a9202 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1788,10 +1788,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): str(netuid), # Number symbol, # Symbol f"{substake_.stake.tao:,.4f} {symbol}", # Stake (a) - f"{pool.tao_in.tao:,.4f} τ", # TAO Reserves (t_in) - f"{pool.alpha_in.tao:,.4f} {symbol}", # Alpha Reserves a_in f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) - f"{pool.alpha_out.tao:,.4f} {symbol}", # Alpha out (a_out) f"{tao_ownership}", # TAO equiv f"{tao_value}", # Exchange Value (α x τ/α) f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ @@ -1817,7 +1814,11 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): show_lines=False, pad_edge=True, ) - table.add_column("[white]Netuid", footer_style="overline white", style="grey89") + table.add_column("[white]Netuid", + footer=f"{len(rows)}", + footer_style="overline white", + style="grey89" + ) table.add_column( "[white]Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], @@ -1830,27 +1831,12 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="center", ) - table.add_column( - f"[white]TAO Reserves ({Balance.unit}_in)", - style=COLOR_PALETTE["STAKE"]["TAO"], - justify="right", - ) - table.add_column( - f"[white]Alpha Reserves ({Balance.get_unit(1)}_in)", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], - justify="right", - ) table.add_column( f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", footer_style="white", style=COLOR_PALETTE["POOLS"]["RATE"], justify="center", ) - table.add_column( - f"[white]Alpha out ({Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["POOLS"]["ALPHA_OUT"], - justify="right", - ) table.add_column( f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index a21f07d4..9ee22d96 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -95,11 +95,11 @@ async def _find_event_attributes_in_extrinsic_receipt( your_balance = your_balance_[wallet.coldkeypub.ss58_address] print_verbose("Fetching burn_cost") - burn_cost = await burn_cost(subtensor) - if burn_cost > your_balance: + sn_burn_cost = await burn_cost(subtensor) + if sn_burn_cost > your_balance: err_console.print( f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}] is not enough to pay the subnet lock cost of: " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}]" + f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}]" ) return False @@ -108,7 +108,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f"Your balance is: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}" ) if not Confirm.ask( - f"Do you want to register a subnet for [{COLOR_PALETTE['POOLS']['TAO']}]{burn_cost}?" + f"Do you want to register a subnet for [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}?" ): return False diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 5def5405..120e6a05 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -229,8 +229,9 @@ async def wallet_balance( """Retrieves the current balance of the specified wallet""" if ss58_addresses: coldkeys = ss58_addresses - wallet_names = [f"Provided Address {i + 1}" for i in range(len(ss58_addresses))] - + identities = await subtensor.query_all_identities() + wallet_names = [f"{identities.get(coldkey, {'name': f'Provided address {i}'})['name']}" for i, coldkey in enumerate(coldkeys)] + elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): err_console.print("[bold red]No wallets found.[/bold red]") From c1195a631593a1909364b8a853c9c62449d17e38 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 20 Dec 2024 11:48:34 -0800 Subject: [PATCH 148/332] Fix sn names --- bittensor_cli/src/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index b44f65ce..1627db0c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -620,7 +620,7 @@ class WalletValidationTypes(Enum): 5: "kaito", 6: "infinite", 7: "subvortex", - 8: "rpn", + 8: "ptn", 9: "pretrain", 10: "sturday", 11: "dippy", @@ -628,7 +628,7 @@ class WalletValidationTypes(Enum): 13: "dataverse", 14: "palaidn", 15: "deval", - 16: "bitrads", + 16: "bitads", 17: "3gen", 18: "cortex", 19: "inference", @@ -670,5 +670,6 @@ class WalletValidationTypes(Enum): 56: "gradients", 57: "gaia", 58: "dippy-speech", - 59: "agent-arena" + 59: "agent-arena", + 61: "red-team", } From 96448c3bb37a3aa4ec11279974e696c92beb6dea Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 20 Dec 2024 16:53:36 -0800 Subject: [PATCH 149/332] Improvements to subnets, stake, sudo --- bittensor_cli/src/commands/stake/stake.py | 4 +- bittensor_cli/src/commands/subnets.py | 52 +++++++++++++---------- bittensor_cli/src/commands/sudo.py | 8 ++-- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index cb1a9202..d58afeb4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1786,8 +1786,8 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): rows.append( [ str(netuid), # Number - symbol, # Symbol - f"{substake_.stake.tao:,.4f} {symbol}", # Stake (a) + symbol if netuid != 0 else "\u03A4", # Symbol + f"{substake_.stake.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {substake_.stake.tao:,.4f}", # Stake (a) f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) f"{tao_ownership}", # TAO equiv f"{tao_value}", # Exchange Value (α x τ/α) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 9ee22d96..71e012f3 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -257,13 +257,13 @@ def create_table(subnets, global_weights, identities): # Prepare cells netuid_cell = str(netuid) - symbol_cell = f"{subnet.symbol}" + symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" subnet_name_cell = SUBNETS.get(netuid, "~") - emission_cell = f"{emission_tao:,.4f}" + emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" - tao_in_cell = f"{subnet.tao_in.tao:,.4f} τ" - alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" - alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" + tao_in_cell = f"τ {subnet.tao_in.tao:,.4f}" + alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" + alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" global_weight_cell = ( f"{global_weight:.4f}" if global_weight is not None else "N/A" @@ -304,22 +304,22 @@ def create_table(subnets, global_weights, identities): # Live mode def create_table_live(subnets, global_weights, identities, previous_data): - def format_cell(value, previous_value, unit="", precision=4): + def format_cell(value, previous_value, unit="", unit_first=False, precision=4): if previous_value is not None: change = value - previous_value - if change > 0: + if change > 0.01: change_text = ( - f" [pale_green3](+{change:.{precision}f}{unit})[/pale_green3]" + f" [pale_green3](+{change:.2f})[/pale_green3]" ) - elif change < 0: + elif change < -0.01: change_text = ( - f" [hot_pink3]({change:.{precision}f}{unit})[/hot_pink3]" + f" [hot_pink3]({change:.2f})[/hot_pink3]" ) else: change_text = "" else: change_text = "" - return f"{value:,.{precision}f}{unit}{change_text}" + return f"{value:,.{precision}f} {unit}{change_text}" if not unit_first else f"{unit} {value:,.{precision}f}{change_text}" rows = [] current_data = {} # To store current values for comparison in the next update @@ -350,27 +350,33 @@ def format_cell(value, previous_value, unit="", precision=4): # Prepare cells netuid_cell = str(netuid) - symbol_cell = f"{subnet.symbol}" + symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" subnet_name_cell = SUBNETS.get(netuid, "~") + if netuid is 0: + unit_first = True + else: + unit_first = False emission_cell = format_cell( - emission_tao, prev.get("emission_tao"), unit="", precision=4 + emission_tao, prev.get("emission_tao"), unit="τ", unit_first=True, precision=4 ) price_cell = format_cell( - subnet.price.tao, prev.get("price"), unit=f" τ/{symbol}", precision=4 + subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4 ) tao_in_cell = format_cell( - subnet.tao_in.tao, prev.get("tao_in"), unit=" τ", precision=4 + subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4 ) alpha_in_cell = format_cell( subnet.alpha_in.tao, prev.get("alpha_in"), - unit=f" {symbol}", + unit=f"{symbol}", + unit_first=unit_first, precision=4, ) alpha_out_cell = format_cell( subnet.alpha_out.tao, prev.get("alpha_out"), - unit=f" {symbol}", + unit=f"{symbol}", + unit_first=unit_first, precision=5, ) @@ -688,10 +694,10 @@ async def show_root(): console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]0 τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.tao_in.tao:,.4f} τ[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.alpha_in.tao:,.4f} {root_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.alpha_out.tao:,.5f} {root_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {root_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol}{root_info.alpha_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {root_info.alpha_out.tao:,.5f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) console.print( @@ -885,8 +891,8 @@ async def show_subnet(netuid_: int): f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.emission.tao:,.4f} τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.tao_in.tao:,.4f} τ[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 7a6b1132..4a9ee5da 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -8,7 +8,7 @@ from rich.prompt import Confirm from scalecodec import GenericCall -from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE +from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE, SUBNETS from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -500,8 +500,10 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): Column("[white]VALUE", style=COLOR_PALETTE['SUDO']['VALUE']), Column("[white]NORMALIZED", style=COLOR_PALETTE['SUDO']['NORMALIZED']), title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" + f"{f' ({SUBNETS.get(netuid)})' if SUBNETS.get(netuid) else ''}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, width=None, pad_edge=False, From f75fa32e3d6879cccb6a017dfa21e45bb89eb207 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 20 Dec 2024 17:17:59 -0800 Subject: [PATCH 150/332] Bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 8fff761c..c3c1da00 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0" +__version__ = "8.2.0+rao.1" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d698039c..04dcde4e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -60,7 +60,7 @@ class GitError(Exception): pass -__version__ = "8.2.0" +__version__ = "8.2.0+rao.1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From b54a1439921c1125816060006e05f5ee738973d7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sun, 22 Dec 2024 08:52:08 -0800 Subject: [PATCH 151/332] Remove warning --- bittensor_cli/src/commands/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 71e012f3..9d5da41d 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -352,7 +352,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): netuid_cell = str(netuid) symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" subnet_name_cell = SUBNETS.get(netuid, "~") - if netuid is 0: + if netuid == 0: unit_first = True else: unit_first = False From 4b5e4f885a0d25bfba223a227696088b116fbef9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sun, 22 Dec 2024 09:28:21 -0800 Subject: [PATCH 152/332] fix --all flag balances --- bittensor_cli/cli.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 04dcde4e..ae8b1ad6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2117,7 +2117,17 @@ def wallet_balance( """ self.verbosity_handler(quiet, verbose) wallet = None - if ss58_addresses: + if all_balances: + ask_for = [WO.PATH] + validate = WV.NONE + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=validate, + ) + elif ss58_addresses: valid_ss58s = [ ss58 for ss58 in set(ss58_addresses) if is_valid_ss58_address(ss58) ] @@ -2156,8 +2166,8 @@ def wallet_balance( raise typer.Exit() else: wallet_name = coldkey_or_ss58_list[0] if coldkey_or_ss58_list else wallet_name - ask_for = [WO.PATH] if all_balances else [WO.NAME, WO.PATH] - validate = WV.NONE if all_balances else WV.WALLET + ask_for = [WO.NAME, WO.PATH] + validate = WV.WALLET wallet = self.wallet_ask( wallet_name, wallet_path, From 47da2f1b54456f96d752c7e50567817baedd5201 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 23 Dec 2024 14:45:28 -0800 Subject: [PATCH 153/332] Adds delegates to st add --- bittensor_cli/cli.py | 33 +++- bittensor_cli/src/commands/subnets.py | 216 ++++++++++++++++++-------- 2 files changed, 179 insertions(+), 70 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ae8b1ad6..7eaef510 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2573,11 +2573,34 @@ def stake_add( "Enter the [blue]wallet name[/blue]", default=self.config.get("wallet_name") or defaults.wallet.name, ) - hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, - ) - if is_valid_ss58_address(hotkey_or_ss58): + if netuid is not None: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim](or Press Enter to view delegates)[/dim]", + ) + else: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + ) + + if hotkey_or_ss58 == "": + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + selected_hotkey = self._run_command( + subnets.show( + subtensor=self.initialize_chain(network), + netuid=netuid, + max_rows=12, + prompt=False, + delegate_selection=True + ) + ) + if selected_hotkey is None: + print_error("No delegate selected. Exiting.") + raise typer.Exit() + include_hotkeys = selected_hotkey + elif is_valid_ss58_address(hotkey_or_ss58) : wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 9d5da41d..39050dfa 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -584,7 +584,14 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): console.print(description_table) -async def show(subtensor: "SubtensorInterface", netuid: int, verbose: bool = False, prompt: bool = True): +async def show( + subtensor: "SubtensorInterface", + netuid: int, + max_rows: Optional[int] = None, + delegate_selection: bool = False, + verbose: bool = False, + prompt: bool = True, +) -> Optional[str]: async def show_root(): all_subnets = await subtensor.get_all_subnet_dynamic_info() root_info = all_subnets[0] @@ -624,6 +631,8 @@ async def show_root(): show_lines=False, pad_edge=True, ) + # if delegate_selection: + # table.add_column("#", style="cyan", justify="right") table.add_column("[bold white]Position", style="white", justify="center") table.add_column( f"[bold white] TAO ({Balance.get_unit(0)})", @@ -661,6 +670,8 @@ async def show_root(): key=lambda x: root_state.global_stake[x[0]], reverse=True, ) + sorted_rows = [] + sorted_hks_delegation = [] for pos, (idx, hk) in enumerate(sorted_hotkeys): total_emission_per_block = 0 for netuid_ in range(len(all_subnets)): @@ -677,42 +688,78 @@ async def show_root(): hotkey_identity = old_identities.get(root_state.hotkeys[idx]) validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") - table.add_row( - str((pos + 1)), - str(root_state.global_stake[idx]), - str(root_state.local_stake[idx]), - f"{(total_emission_per_block)}", - f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", - f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", - validator_identity, + sorted_rows.append( + ( + str((pos + 1)), + str(root_state.global_stake[idx]), + str(root_state.local_stake[idx]), + f"{(total_emission_per_block)}", + f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", + f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", + validator_identity, + ) ) - + sorted_hks_delegation.append(root_state.hotkeys[idx]) + + for pos, row in enumerate(sorted_rows, 1): + table_row = [] + # if delegate_selection: + # table_row.append(str(pos)) + table_row.extend(row) + table.add_row(*table_row) + if delegate_selection and pos == max_rows: + break # Print the table console.print(table) console.print("\n") - console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {root_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol}{root_info.alpha_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {root_info.alpha_out.tao:,.5f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - ) - console.print( - """ -Description: - The table displays the root subnet participants and their metrics. - The columns are as follows: - - Position: The sorted position of the hotkey by total TAO. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. - - Stake: The stake balance of this hotkey on root (measured in TAO). - - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - - Hotkey: The hotkey ss58 address. - - Coldkey: The coldkey ss58 address. -""" - ) + if not delegate_selection: + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {root_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol}{root_info.alpha_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {root_info.alpha_out.tao:,.5f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + ) + console.print( + """ + Description: + The table displays the root subnet participants and their metrics. + The columns are as follows: + - Position: The sorted position of the hotkey by total TAO. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on root (measured in TAO). + - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. + - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. + """ + ) + if delegate_selection: + while True: + selection = Prompt.ask( + "\nEnter the position of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + default="" + ) + + if selection == "": + return None + + try: + idx = int(selection) + if 1 <= idx <= max_rows: + selected_hotkey = sorted_hks_delegation[idx - 1] + row_data = sorted_rows[idx - 1] + identity = row_data[6] + identity_str = f" ({identity})" if identity else "" + console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}") + + return selected_hotkey + else: + console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") async def show_subnet(netuid_: int): subnet_info, hex_bytes_result, identities, old_identities = await asyncio.gather( @@ -759,6 +806,11 @@ async def show_subnet(netuid_: int): show_lines=False, pad_edge=True, ) + + # Add index for selection if selecting delegates + if delegate_selection: + table.add_column("#", style="cyan", justify="right") + rows = [] emission_sum = sum( [ @@ -876,50 +928,84 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="left", ) - for row in sorted_rows: - table.add_row(*row) + for pos, row in enumerate(sorted_rows, 1): + table_row = [] + if delegate_selection: + table_row.append(str(pos)) + table_row.extend(row) + table.add_row(*table_row) + if delegate_selection and pos == max_rows: + break # Print the table console.print("\n\n") console.print(table) console.print("\n") - subnet_name = SUBNETS.get(netuid_, '') - subnet_name_display = f": {subnet_name}" if subnet_name else "" + if not delegate_selection: + subnet_name = SUBNETS.get(netuid_, '') + subnet_name_display = f": {subnet_name}" if subnet_name else "" - console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - ) - console.print( - """ -Description: - The table displays the subnet participants and their metrics. - The columns are as follows: - - UID: The hotkey index in the subnet. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. - - Stake: The stake balance of this hotkey on this subnet. - - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. - - Dividends: Validating dividends earned by the hotkey. - - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) - - Emission: The emission accrued to this hokey on this subnet every block (in staking units). - - Hotkey: The hotkey ss58 address. - - Coldkey: The coldkey ss58 address. -""" - ) + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + ) + console.print( + """ + Description: + The table displays the subnet participants and their metrics. + The columns are as follows: + - UID: The hotkey index in the subnet. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on this subnet. + - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. + - Dividends: Validating dividends earned by the hotkey. + - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) + - Emission: The emission accrued to this hokey on this subnet every block (in staking units). + - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. + """ + ) + if delegate_selection: + while True: + selection = Prompt.ask( + "\nEnter the number of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + default="" + ) + + if selection == "": + return None + + try: + idx = int(selection) + if 1 <= idx <= max_rows: + uid = int(sorted_rows[idx-1][0]) + hotkey = subnet_state.hotkeys[uid] + row_data = sorted_rows[idx-1] + identity = row_data[9] + identity_str = f" ({identity})" if identity else "" + console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}") + return hotkey + else: + console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + + return None if netuid == 0: - await show_root() + result = await show_root() + return result else: - await show_subnet(netuid) - + result = await show_subnet(netuid) + return result async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: """View locking cost of creating a new subnetwork""" From 14688db867132cd92287fc0d28b9f3eb4ee67079 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 23 Dec 2024 18:15:55 -0800 Subject: [PATCH 154/332] Improvement: stake remove --- bittensor_cli/cli.py | 19 +- bittensor_cli/src/commands/stake/stake.py | 222 +++++++++++++++++++--- 2 files changed, 208 insertions(+), 33 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7eaef510..e358d335 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2769,7 +2769,7 @@ def stake_remove( ) raise typer.Exit() - if not interactive: + if not interactive and not unstake_all: netuid = get_optional_netuid(netuid, all_netuids) if all_hotkeys and include_hotkeys: err_console.print( @@ -2800,6 +2800,7 @@ def stake_remove( and not all_hotkeys and not include_hotkeys and not interactive + and not unstake_all ): if not wallet_name: wallet_name = Prompt.ask( @@ -2807,10 +2808,14 @@ def stake_remove( default=self.config.get("wallet_name") or defaults.wallet.name, ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, - ) - if is_valid_ss58_address(hotkey_or_ss58): + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", + ) + if hotkey_or_ss58 == "": + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + interactive = True + elif is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] @@ -2831,11 +2836,11 @@ def stake_remove( or exclude_hotkeys or hotkey_ss58_address or interactive + or unstake_all ): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - else: wallet = self.wallet_ask( wallet_name, @@ -2870,7 +2875,6 @@ def stake_remove( wallet, self.initialize_chain(network), hotkey_ss58_address, - netuid, all_hotkeys, included_hotkeys, excluded_hotkeys, @@ -2879,6 +2883,7 @@ def stake_remove( unstake_all, prompt, interactive, + netuid=netuid, ) ) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index d58afeb4..ebe26b02 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1158,6 +1158,7 @@ async def unstake_selection( dynamic_info, identities, old_identities, + netuid: Optional[int] = None, ): stake_infos = await subtensor.get_stake_info_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address @@ -1169,11 +1170,20 @@ async def unstake_selection( hotkey_stakes = {} for stake_info in stake_infos: + if netuid is not None and stake_info.netuid != netuid: + continue hotkey_ss58 = stake_info.hotkey_ss58 netuid_ = stake_info.netuid stake_amount = stake_info.stake if stake_amount.tao > 0: hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount + + if not hotkey_stakes: + if netuid is not None: + print_error(f"You have no stakes to unstake in subnet {netuid}.") + else: + print_error("You have no stakes to unstake.") + return hotkeys_info = [] for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): @@ -1197,8 +1207,9 @@ async def unstake_selection( ) # Display existing hotkeys, id, and staked netuids. + subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1258,33 +1269,36 @@ async def unstake_selection( symbol = dynamic_info[netuid_].symbol rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" table.add_row(str(netuid_), symbol, str(stake_amount), rate) - console.print("\n", table) + console.print("\n", table, "\n") # Ask which netuids to unstake from for the selected hotkey. - while True: - netuid_input = Prompt.ask( - "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", - default="all", - ) + if netuid is not None: + selected_netuids = [netuid] + else: + while True: + netuid_input = Prompt.ask( + "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", + default="all", + ) - if netuid_input.lower() == "all": - selected_netuids = list(netuid_stakes.keys()) - break - else: - try: - netuid_list = [int(n.strip()) for n in netuid_input.split(",")] - invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] - if invalid_netuids: + if netuid_input.lower() == "all": + selected_netuids = list(netuid_stakes.keys()) + break + else: + try: + netuid_list = [int(n.strip()) for n in netuid_input.split(",")] + invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] + if invalid_netuids: + print_error( + f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + ) + else: + selected_netuids = netuid_list + break + except ValueError: print_error( - f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + "Please enter valid netuids (numbers), separated by commas, or 'all'." ) - else: - selected_netuids = netuid_list - break - except ValueError: - print_error( - "Please enter valid netuids (numbers), separated by commas, or 'all'." - ) hotkeys_to_unstake_from = [] for netuid_ in selected_netuids: @@ -1353,11 +1367,162 @@ def ask_unstake_amount( console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]") +async def _unstake_all( + wallet: Wallet, + subtensor: "SubtensorInterface", + prompt: bool = True, +) -> bool: + """Unstakes all stakes from all hotkeys in all subnets.""" + + with console.status( + f"Retrieving stake information & identities from {subtensor.network}...", + spinner="earth", + ): + stake_info, ck_hk_identities, old_identities, all_sn_dynamic_info_, current_wallet_balance = await asyncio.gather( + subtensor.get_stake_info_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + subtensor.get_all_subnet_dynamic_info(), + subtensor.get_balance(wallet.coldkeypub.ss58_address) + ) + + if not stake_info: + console.print("[red]No stakes found to unstake[/red]") + return False + + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + # Calculate total value and slippage for all stakes + total_received_value = Balance(0) + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking Summary - All Stakes\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Current Stake ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + ) + table.add_column( + f"Rate ({Balance.unit}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"Recieved ({Balance.unit})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + max_slippage = 0.0 + for stake in stake_info: + if stake.stake.rao == 0: + continue + + dynamic_info = all_sn_dynamic_info.get(stake.netuid) + stake_amount = stake.stake + received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage(stake_amount) + + total_received_value += received_amount + + # Get hotkey identity + identity = ck_hk_identities["hotkeys"].get(stake.hotkey_ss58) or old_identities.get(stake.hotkey_ss58) + hotkey_display = stake.hotkey_ss58 + if identity: + hotkey_name = identity.get("identity", {}).get("name", "") or identity.get("display", "~") + hotkey_display = f"{hotkey_name}" + + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "[red]N/A[/red]" + + max_slippage = max(max_slippage, slippage_pct_float) + + table.add_row( + str(stake.netuid), + hotkey_display, + str(stake_amount), + str(float(dynamic_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", + str(received_amount), + slippage_pct, + ) + console.print(table) + message = "" + if max_slippage > 5: + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + console.print( + f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" + ) + + if prompt and not Confirm.ask("\nDo you want to proceed with unstaking everything?"): + return False + + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status(":satellite: Unstaking all stakes..."): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="unstake_all", + call_params={}, + ) + + success, error_message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + if success: + console.print(":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]") + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + console.print( + f"Balance:\n [blue]{current_wallet_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + return True + else: + err_console.print(f":cross_mark: [red]Failed to unstake[/red]: {error_message}") + return False + + async def unstake( wallet: Wallet, subtensor: "SubtensorInterface", hotkey_ss58_address: str, - netuid: Optional[int], all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], @@ -1366,8 +1531,13 @@ async def unstake( unstake_all: bool, prompt: bool, interactive: bool = False, + netuid: Optional[int] = None, ): """Unstake tokens from hotkey(s).""" + + if unstake_all: + return await _unstake_all(wallet, subtensor, prompt) + with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", @@ -1381,7 +1551,7 @@ async def unstake( if interactive: hotkeys_to_unstake_from = await unstake_selection( - subtensor, wallet, all_sn_dynamic_info, ck_hk_identities, old_identities + subtensor, wallet, all_sn_dynamic_info, ck_hk_identities, old_identities, netuid=netuid ) if not hotkeys_to_unstake_from: console.print("[red]No unstake operations to perform.[/red]") @@ -1537,7 +1707,7 @@ async def unstake( slippage_pct = f"{slippage_pct_float:.4f} %" else: slippage_pct_float = 0 - slippage_pct = f"{slippage_pct_float}%" + slippage_pct = "[red]N/A[/red]" max_float_slippage = max(max_float_slippage, slippage_pct_float) unstake_operations.append( From be875e22ba60e7ed3197eb7558d1c00c62aca503 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 23 Dec 2024 21:31:16 -0800 Subject: [PATCH 155/332] Feat: wallet with uris --- bittensor_cli/cli.py | 53 +++++++++----- bittensor_cli/src/bittensor/utils.py | 11 +++ bittensor_cli/src/commands/wallets.py | 99 +++++++++++++++++++-------- 3 files changed, 118 insertions(+), 45 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e358d335..0e2ae56f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -46,6 +46,7 @@ is_rao_network, get_effective_network, prompt_for_identity, + validate_uri, ) from typing_extensions import Annotated from textwrap import dedent @@ -227,6 +228,12 @@ class Options: "--live", help="Display live view of the table", ) + uri = typer.Option( + None, + "--uri", + help="Create wallet from uri (e.g. 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve')", + callback=validate_uri, + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -598,7 +605,9 @@ def __init__(self): ) # utils app - self.app.add_typer(self.utils_app, name="utils", no_args_is_help=True, hidden=True) + self.app.add_typer( + self.utils_app, name="utils", no_args_is_help=True, hidden=True + ) # config commands self.config_app.command("set")(self.set_config) @@ -1902,6 +1911,7 @@ def wallet_new_hotkey( is_flag=True, flag_value=True, ), + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1940,8 +1950,9 @@ def wallet_new_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET, ) - n_words = get_n_words(n_words) - return self._run_command(wallets.new_hotkey(wallet, n_words, use_password)) + if not uri: + n_words = get_n_words(n_words) + return self._run_command(wallets.new_hotkey(wallet, n_words, use_password, uri)) def wallet_new_coldkey( self, @@ -1955,6 +1966,7 @@ def wallet_new_coldkey( help="The number of words used in the mnemonic. Options: [12, 15, 18, 21, 24]", ), use_password: Optional[bool] = Options.use_password, + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1991,8 +2003,11 @@ def wallet_new_coldkey( ask_for=[WO.NAME, WO.PATH], validate=WV.NONE, ) - n_words = get_n_words(n_words) - return self._run_command(wallets.new_coldkey(wallet, n_words, use_password)) + if not uri: + n_words = get_n_words(n_words) + return self._run_command( + wallets.new_coldkey(wallet, n_words, use_password, uri) + ) def wallet_check_ck_swap( self, @@ -2026,6 +2041,7 @@ def wallet_create_wallet( wallet_hotkey: Optional[str] = Options.wallet_hotkey, n_words: Optional[int] = None, use_password: bool = Options.use_password, + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2066,12 +2082,14 @@ def wallet_create_wallet( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.NONE, ) - n_words = get_n_words(n_words) + if not uri: + n_words = get_n_words(n_words) return self._run_command( wallets.wallet_create( wallet, n_words, use_password, + uri, ) ) @@ -2148,10 +2166,9 @@ def wallet_balance( "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 addresses[/blue] (comma-separated)", default=self.config.get("wallet_name") or defaults.wallet.name, ) - # Split by comma and strip whitespace + + # Split and validate ss58 addresses coldkey_or_ss58_list = [x.strip() for x in coldkey_or_ss58.split(",")] - - # Check if any entry is a valid SS58 address if any(is_valid_ss58_address(x) for x in coldkey_or_ss58_list): valid_ss58s = [ ss58 for ss58 in coldkey_or_ss58_list if is_valid_ss58_address(ss58) @@ -2159,13 +2176,15 @@ def wallet_balance( invalid_ss58s = set(coldkey_or_ss58_list) - set(valid_ss58s) for invalid_ss58 in invalid_ss58s: print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") - + if valid_ss58s: ss58_addresses = valid_ss58s else: raise typer.Exit() else: - wallet_name = coldkey_or_ss58_list[0] if coldkey_or_ss58_list else wallet_name + wallet_name = ( + coldkey_or_ss58_list[0] if coldkey_or_ss58_list else wallet_name + ) ask_for = [WO.NAME, WO.PATH] validate = WV.WALLET wallet = self.wallet_ask( @@ -2588,19 +2607,19 @@ def stake_add( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) selected_hotkey = self._run_command( - subnets.show( + subnets.show( subtensor=self.initialize_chain(network), netuid=netuid, max_rows=12, - prompt=False, - delegate_selection=True + prompt=False, + delegate_selection=True, ) ) if selected_hotkey is None: print_error("No delegate selected. Exiting.") raise typer.Exit() include_hotkeys = selected_hotkey - elif is_valid_ss58_address(hotkey_or_ss58) : + elif is_valid_ss58_address(hotkey_or_ss58): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) @@ -2808,8 +2827,8 @@ def stake_remove( default=self.config.get("wallet_name") or defaults.wallet.name, ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", - ) + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", + ) if hotkey_or_ss58 == "": wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 95cf5371..7987db00 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -986,6 +986,17 @@ def validate_netuid(value: int) -> int: return value +def validate_uri(uri: str) -> str: + if not uri: + raise ValueError("URI cannot be empty") + clean_uri = uri.lstrip("/").lower() + if not clean_uri.isalnum(): + raise typer.BadParameter( + f"Invalid URI format: {uri}. URI must contain only alphanumeric characters (e.g. 'alice', 'bob')" + ) + return f"//{clean_uri.capitalize()}" + + def get_effective_network(config, network: Optional[list[str]]) -> str: """ Determines the effective network to be used, considering the network parameter, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 120e6a05..34e5399a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -10,7 +10,7 @@ from typing import Any, Collection, Generator, Optional import aiohttp -from bittensor_wallet import Wallet +from bittensor_wallet import Wallet, Keypair from bittensor_wallet.errors import KeyFileError from bittensor_wallet.keyfile import Keyfile from fuzzywuzzy import fuzz @@ -135,14 +135,26 @@ async def new_hotkey( wallet: Wallet, n_words: int, use_password: bool, + uri: Optional[str] = None, ): """Creates a new hotkey under this wallet.""" try: - wallet.create_new_hotkey( - n_words=n_words, - use_password=use_password, - overwrite=False, - ) + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + wallet.set_hotkey(keypair=keypair, encrypt=use_password) + console.print( + f"[dark_sea_green]Hotkey created from URI: {uri}[/dark_sea_green]" + ) + else: + wallet.create_new_hotkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -151,14 +163,27 @@ async def new_coldkey( wallet: Wallet, n_words: int, use_password: bool, + uri: Optional[str] = None, ): """Creates a new coldkey under this wallet.""" try: - wallet.create_new_coldkey( - n_words=n_words, - use_password=use_password, - overwrite=False, - ) + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) + console.print( + f"[dark_sea_green]Coldkey created from URI: {uri}[/dark_sea_green]" + ) + else: + wallet.create_new_coldkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -167,25 +192,40 @@ async def wallet_create( wallet: Wallet, n_words: int = 12, use_password: bool = True, + uri: Optional[str] = None, ): """Creates a new wallet.""" - try: - wallet.create_new_coldkey( - n_words=n_words, - use_password=use_password, - overwrite=False, + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI: {str(e)}") + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) + console.print( + f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]" ) - except KeyFileError: - print_error("KeyFileError: File is not writable") + else: + try: + wallet.create_new_coldkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") + except KeyFileError: + print_error("KeyFileError: File is not writable") - try: - wallet.create_new_hotkey( - n_words=n_words, - use_password=False, - overwrite=False, - ) - except KeyFileError: - print_error("KeyFileError: File is not writable") + try: + wallet.create_new_hotkey( + n_words=n_words, + use_password=False, + overwrite=False, + ) + console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") + except KeyFileError: + print_error("KeyFileError: File is not writable") def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: @@ -230,8 +270,11 @@ async def wallet_balance( if ss58_addresses: coldkeys = ss58_addresses identities = await subtensor.query_all_identities() - wallet_names = [f"{identities.get(coldkey, {'name': f'Provided address {i}'})['name']}" for i, coldkey in enumerate(coldkeys)] - + wallet_names = [ + f"{identities.get(coldkey, {'name': f'Provided address {i}'})['name']}" + for i, coldkey in enumerate(coldkeys) + ] + elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): err_console.print("[bold red]No wallets found.[/bold red]") From a44fa7ca29dd9b100e793e2cada3ed9266d82e3a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 24 Dec 2024 12:41:00 -0800 Subject: [PATCH 156/332] Adds unstake_all_alpha --- bittensor_cli/cli.py | 17 +++- bittensor_cli/src/commands/stake/stake.py | 115 +++++++++++++++------- 2 files changed, 93 insertions(+), 39 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0e2ae56f..20e030f2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2724,7 +2724,13 @@ def stake_remove( False, "--unstake-all", "--all", - help="When set, this command unstakes all staked TAO from the specified hotkeys.", + help="When set, this command unstakes all staked TAO + Alpha from the all hotkeys.", + ), + unstake_all_alpha: bool = typer.Option( + False, + "--unstake-all-alpha", + "--all-alpha", + help="When set, this command unstakes all staked Alpha from the all hotkeys.", ), amount: float = typer.Option( 0.0, "--amount", "-a", help="The amount of TAO to unstake." @@ -2788,7 +2794,11 @@ def stake_remove( ) raise typer.Exit() - if not interactive and not unstake_all: + if unstake_all and unstake_all_alpha: + err_console.print("Cannot specify both unstake-all and unstake-all-alpha.") + raise typer.Exit() + + if not interactive and not unstake_all and not unstake_all_alpha: netuid = get_optional_netuid(netuid, all_netuids) if all_hotkeys and include_hotkeys: err_console.print( @@ -2820,6 +2830,7 @@ def stake_remove( and not include_hotkeys and not interactive and not unstake_all + and not unstake_all_alpha ): if not wallet_name: wallet_name = Prompt.ask( @@ -2856,6 +2867,7 @@ def stake_remove( or hotkey_ss58_address or interactive or unstake_all + or unstake_all_alpha ): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] @@ -2903,6 +2915,7 @@ def stake_remove( prompt, interactive, netuid=netuid, + unstake_all_alpha=unstake_all_alpha, ) ) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index ebe26b02..dc259192 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1177,7 +1177,7 @@ async def unstake_selection( stake_amount = stake_info.stake if stake_amount.tao > 0: hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount - + if not hotkey_stakes: if netuid is not None: print_error(f"You have no stakes to unstake in subnet {netuid}.") @@ -1370,22 +1370,32 @@ def ask_unstake_amount( async def _unstake_all( wallet: Wallet, subtensor: "SubtensorInterface", + unstake_all_alpha: bool = False, prompt: bool = True, ) -> bool: """Unstakes all stakes from all hotkeys in all subnets.""" - + with console.status( f"Retrieving stake information & identities from {subtensor.network}...", spinner="earth", ): - stake_info, ck_hk_identities, old_identities, all_sn_dynamic_info_, current_wallet_balance = await asyncio.gather( + ( + stake_info, + ck_hk_identities, + old_identities, + all_sn_dynamic_info_, + current_wallet_balance, + ) = await asyncio.gather( subtensor.get_stake_info_for_coldkey(wallet.coldkeypub.ss58_address), subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), subtensor.get_all_subnet_dynamic_info(), - subtensor.get_balance(wallet.coldkeypub.ss58_address) + subtensor.get_balance(wallet.coldkeypub.ss58_address), ) - + + if unstake_all_alpha: + stake_info = [stake for stake in stake_info if stake.netuid != 0] + if not stake_info: console.print("[red]No stakes found to unstake[/red]") return False @@ -1394,8 +1404,13 @@ async def _unstake_all( # Calculate total value and slippage for all stakes total_received_value = Balance(0) + table_title = ( + "Unstaking Summary - All Stakes" + if not unstake_all_alpha + else "Unstaking Summary - All Alpha Stakes" + ) table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking Summary - All Stakes\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1427,26 +1442,32 @@ async def _unstake_all( table.add_column( "Slippage", justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], ) max_slippage = 0.0 for stake in stake_info: if stake.stake.rao == 0: continue - + dynamic_info = all_sn_dynamic_info.get(stake.netuid) stake_amount = stake.stake - received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage(stake_amount) - + received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( + stake_amount + ) + total_received_value += received_amount - + # Get hotkey identity - identity = ck_hk_identities["hotkeys"].get(stake.hotkey_ss58) or old_identities.get(stake.hotkey_ss58) + identity = ck_hk_identities["hotkeys"].get( + stake.hotkey_ss58 + ) or old_identities.get(stake.hotkey_ss58) hotkey_display = stake.hotkey_ss58 if identity: - hotkey_name = identity.get("identity", {}).get("name", "") or identity.get("display", "~") + hotkey_name = identity.get("identity", {}).get( + "name", "" + ) or identity.get("display", "~") hotkey_display = f"{hotkey_name}" - + if dynamic_info.is_dynamic: slippage_pct_float = ( 100 * float(slippage) / float(slippage + received_amount) @@ -1457,9 +1478,9 @@ async def _unstake_all( else: slippage_pct_float = 0 slippage_pct = "[red]N/A[/red]" - + max_slippage = max(max_slippage, slippage_pct_float) - + table.add_row( str(stake.netuid), hotkey_display, @@ -1476,12 +1497,14 @@ async def _unstake_all( message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" message += "-------------------------------------------------------------------------------------------------------------------\n" console.print(message) - + console.print( f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" ) - - if prompt and not Confirm.ask("\nDo you want to proceed with unstaking everything?"): + + if prompt and not Confirm.ask( + "\nDo you want to proceed with unstaking everything?" + ): return False try: @@ -1490,13 +1513,18 @@ async def _unstake_all( err_console.print("Error decrypting coldkey (possibly incorrect password)") return False - with console.status(":satellite: Unstaking all stakes..."): + console_status = ( + ":satellite: Unstaking all Alpha stakes..." + if unstake_all_alpha + else ":satellite: Unstaking all stakes..." + ) + with console.status(console_status): + call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="unstake_all", + call_function=call_function, call_params={}, ) - success, error_message = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -1505,17 +1533,22 @@ async def _unstake_all( ) if success: - console.print(":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]") - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address + success_message = ( + ":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]" + if not unstake_all_alpha + else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" ) + console.print(success_message) + new_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) new_balance = new_balance_[wallet.coldkeypub.ss58_address] console.print( f"Balance:\n [blue]{current_wallet_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) return True else: - err_console.print(f":cross_mark: [red]Failed to unstake[/red]: {error_message}") + err_console.print( + f":cross_mark: [red]Failed to unstake[/red]: {error_message}" + ) return False @@ -1532,11 +1565,12 @@ async def unstake( prompt: bool, interactive: bool = False, netuid: Optional[int] = None, + unstake_all_alpha: bool = False, ): """Unstake tokens from hotkey(s).""" - if unstake_all: - return await _unstake_all(wallet, subtensor, prompt) + if unstake_all or unstake_all_alpha: + return await _unstake_all(wallet, subtensor, unstake_all_alpha, prompt) with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", @@ -1551,7 +1585,12 @@ async def unstake( if interactive: hotkeys_to_unstake_from = await unstake_selection( - subtensor, wallet, all_sn_dynamic_info, ck_hk_identities, old_identities, netuid=netuid + subtensor, + wallet, + all_sn_dynamic_info, + ck_hk_identities, + old_identities, + netuid=netuid, ) if not hotkeys_to_unstake_from: console.print("[red]No unstake operations to perform.[/red]") @@ -1956,8 +1995,10 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): rows.append( [ str(netuid), # Number - symbol if netuid != 0 else "\u03A4", # Symbol - f"{substake_.stake.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {substake_.stake.tao:,.4f}", # Stake (a) + symbol if netuid != 0 else "\u03a4", # Symbol + f"{substake_.stake.tao:,.4f} {symbol}" + if netuid != 0 + else f"{symbol} {substake_.stake.tao:,.4f}", # Stake (a) f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) f"{tao_ownership}", # TAO equiv f"{tao_value}", # Exchange Value (α x τ/α) @@ -1965,9 +2006,8 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): "YES" if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered - str(Balance.from_tao(per_block_emission).set_unit(netuid)) + str(Balance.from_tao(per_block_emission).set_unit(netuid)), # Removing this flag for now, TODO: Confirm correct values are here w.r.t CHKs - # if substake_.is_registered # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) ] @@ -1984,10 +2024,11 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): show_lines=False, pad_edge=True, ) - table.add_column("[white]Netuid", - footer=f"{len(rows)}", - footer_style="overline white", - style="grey89" + table.add_column( + "[white]Netuid", + footer=f"{len(rows)}", + footer_style="overline white", + style="grey89", ) table.add_column( "[white]Symbol", From bc1c260c2bed7dedc6f985e8c337ae0d9d0ba256 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 24 Dec 2024 20:32:57 -0800 Subject: [PATCH 157/332] Live mode: stake list --- bittensor_cli/cli.py | 3 +- bittensor_cli/src/commands/stake/stake.py | 693 ++++++++++++++++------ 2 files changed, 514 insertions(+), 182 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 20e030f2..1781830f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2470,6 +2470,7 @@ def stake_list( "--coldkey.ss58", help="Coldkey address of the wallet", ), + live: bool = Options.live, quiet: bool = Options.quiet, verbose: bool = Options.verbose, # TODO add: all-wallets, reuse_last, html_output @@ -2499,7 +2500,7 @@ def stake_list( ) return self._run_command( - stake.stake_list(wallet, coldkey_ss58, self.initialize_chain(network)) + stake.stake_list(wallet, coldkey_ss58, self.initialize_chain(network), live) ) def stake_add( diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index dc259192..b22a7513 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -9,6 +9,9 @@ from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Table from rich import box +from rich.progress import Progress, BarColumn, TextColumn +from rich.console import Console, Group +from rich.live import Live from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE @@ -1916,29 +1919,116 @@ async def unstake( async def stake_list( - wallet: Wallet, coldkey_ss58: str, subtensor: "SubtensorInterface" + wallet: Wallet, + coldkey_ss58: str, + subtensor: "SubtensorInterface", + live: bool = False, ): coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address - sub_stakes = ( - await subtensor.get_stake_info_for_coldkeys(coldkey_ss58_list=[coldkey_address]) - )[coldkey_address] - - # Get registered delegates details. - registered_delegate_info = await subtensor.get_delegate_identities() - - # Token pricing info. - dynamic_info = await subtensor.get_all_subnet_dynamic_info() - emission_drain_tempo = int( - await subtensor.substrate.query("SubtensorModule", "HotkeyEmissionTempo") - ) - balance = (await subtensor.get_balance(coldkey_address))[coldkey_address] - - # Iterate over substakes and aggregate them by hotkey. - hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + async def get_stake_data(block_hash: str = None): + ( + substakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) = await asyncio.gather( + subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[coldkey_address], block_hash=block_hash + ), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_all_subnet_dynamic_info(), + subtensor.substrate.query( + "SubtensorModule", "HotkeyEmissionTempo", block_hash=block_hash + ), + ) + sub_stakes = substakes[coldkey_address] + return ( + sub_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) + + def define_table( + hotkey_name: str, + rows: list[list[str]], + total_tao_ownership: Balance, + total_tao_value: Balance, + total_swapped_tao_value: Balance, + live: bool = False, + ): + title = f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {hotkey_name}\nNetwork: {subtensor.network}\n\n" + if not live: + title += f"[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n" + table = Table( + title=title, + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column( + "[white]Netuid", + footer=f"{len(rows)}", + footer_style="overline white", + style="grey89", + ) + table.add_column( + "[white]Symbol", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="center", + no_wrap=True, + ) + table.add_column( + f"[white]Stake ({Balance.get_unit(1)})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="center", + ) + table.add_column( + f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", + footer_style="white", + style=COLOR_PALETTE["POOLS"]["RATE"], + justify="center", + ) + table.add_column( + f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + justify="right", + footer=f"{total_tao_ownership}", + ) + table.add_column( + f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="right", + footer=f"{total_tao_value}", + ) + table.add_column( + f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], + justify="right", + footer=f"{total_swapped_tao_value}", + ) + table.add_column( + "[white]Registered", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="right", + ) + table.add_column( + f"[white]Emission \n({Balance.get_unit(1)}/block)", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="right", + ) + return table - def table_substakes(hotkey_: str, substakes: list[StakeInfo]): - # Create table structure. + def create_table(hotkey_: str, substakes: list[StakeInfo]): name = ( f"{registered_delegate_info[hotkey_].display} ({hotkey_})" if hotkey_ in registered_delegate_info @@ -1952,6 +2042,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): netuid = substake_.netuid pool = dynamic_info[netuid] symbol = f"{Balance.get_unit(netuid)}\u200e" + # TODO: what is this price var for? price = ( "{:.4f}{}".format( @@ -1960,13 +2051,21 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): if pool.is_dynamic else (f" 1.0000 τ/{symbol} ") ) + + # Alpha value cell alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid) + + # TAO value cell tao_value = pool.alpha_to_tao(alpha_value) total_tao_value += tao_value + + # Swapped TAO value and slippage cell swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( substake_.stake ) total_swapped_tao_value += swapped_tao_value + + # Slippage percentage cell if pool.is_dynamic: slippage_percentage_ = ( 100 * float(slippage) / float(slippage + swapped_tao_value) @@ -1976,9 +2075,17 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" else: slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" + + # TAO locked cell tao_locked = pool.tao_in + + # Issuance cell issuance = pool.alpha_out if pool.is_dynamic else tao_locked + + # Per block emission cell per_block_emission = substake_.emission.tao / emission_drain_tempo + + # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: if issuance.tao != 0: alpha_ownership = "{:.4f}".format( @@ -1992,6 +2099,7 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): # TODO what's this var for? alpha_ownership = "0.0000" tao_ownership = "0.0000" + rows.append( [ str(netuid), # Number @@ -2012,77 +2120,192 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) ] ) - # table = Table(show_footer=True, pad_edge=False, box=None, expand=False, title=f"{name}") - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {name}\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n\n[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column( - "[white]Netuid", - footer=f"{len(rows)}", - footer_style="overline white", - style="grey89", - ) - table.add_column( - "[white]Symbol", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], - justify="center", - no_wrap=True, - ) - table.add_column( - f"[white]Stake ({Balance.get_unit(1)})", - footer_style="overline white", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], - justify="center", - ) - table.add_column( - f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", - footer_style="white", - style=COLOR_PALETTE["POOLS"]["RATE"], - justify="center", - ) - table.add_column( - f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - justify="right", - footer=f"{total_tao_ownership}", - ) - table.add_column( - f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", - footer_style="overline white", - style=COLOR_PALETTE["STAKE"]["TAO"], - justify="right", - footer=f"{total_tao_value}", - ) - table.add_column( - f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", - footer_style="overline white", - style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], - justify="right", - footer=f"{total_swapped_tao_value}", - ) - table.add_column( - "[white]Registered", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], - justify="right", - ) - table.add_column( - f"[white]Emission \n({Balance.get_unit(1)}/block)", - style=COLOR_PALETTE["POOLS"]["EMISSION"], - justify="right", + table = define_table( + name, rows, total_tao_ownership, total_tao_value, total_swapped_tao_value ) for row in rows: table.add_row(*row) console.print(table) return total_tao_ownership, total_tao_value + def create_live_table( + substakes: list, + registered_delegate_info: dict, + dynamic_info: dict, + emission_drain_tempo: int, + hotkey_name: str, + previous_data: Optional[dict] = None, + ) -> tuple[Table, dict, Balance, Balance, Balance]: + rows = [] + current_data = {} + + total_tao_ownership = Balance(0) + total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) + + def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + if previous_value is not None: + change = value - previous_value + if abs(change) > 10 ** (-precision): + change_text = ( + f" [pale_green3](+{change:.{precision}f})[/pale_green3]" + if change > 0 + else f" [hot_pink3]({change:.{precision}f})[/hot_pink3]" + ) + else: + change_text = "" + else: + change_text = "" + return ( + f"{value:,.{precision}f} {unit}{change_text}" + if not unit_first + else f"{unit} {value:,.{precision}f}{change_text}" + ) + + # Process each stake + for substake in substakes: + netuid = substake.netuid + pool = dynamic_info.get(netuid) + if substake.stake.rao == 0 or not pool: + continue + + # Calculate base values + symbol = f"{Balance.get_unit(netuid)}\u200e" + alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid) + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value + swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( + substake.stake + ) + total_swapped_tao_value += swapped_tao_value + + # Calculate TAO ownership + tao_locked = pool.tao_in + issuance = pool.alpha_out if pool.is_dynamic else tao_locked + if alpha_value.tao > 0.00009 and issuance.tao != 0: + tao_ownership = Balance.from_tao( + (alpha_value.tao / issuance.tao) * tao_locked.tao + ) + total_tao_ownership += tao_ownership + else: + tao_ownership = Balance.from_tao(0) + + # Store current values for future delta tracking + current_data[netuid] = { + "stake": alpha_value.tao, + "price": pool.price.tao, + "tao_value": tao_value.tao, + "swapped_value": swapped_tao_value.tao, + "emission": substake.emission.tao / emission_drain_tempo, + "tao_ownership": tao_ownership.tao, + } + + # Get previous values for delta tracking + prev = previous_data.get(netuid, {}) if previous_data else {} + unit_first = True if netuid == 0 else False + + stake_cell = format_cell( + alpha_value.tao, + prev.get("stake"), + unit=symbol, + unit_first=unit_first, + precision=4, + ) + + rate_cell = format_cell( + pool.price.tao, + prev.get("price"), + unit=f"τ/{symbol}", + unit_first=False, + precision=5, + ) + + tao_ownership_cell = format_cell( + tao_ownership.tao, + prev.get("tao_ownership"), + unit="τ", + unit_first=True, + precision=4, + ) + + exchange_cell = format_cell( + tao_value.tao, + prev.get("tao_value"), + unit="τ", + unit_first=True, + precision=4, + ) + + if pool.is_dynamic: + slippage_pct = ( + 100 * float(slippage) / float(slippage + swapped_tao_value) + if slippage + swapped_tao_value != 0 + else 0 + ) + else: + slippage_pct = 0 + + swap_cell = ( + format_cell( + swapped_tao_value.tao, + prev.get("swapped_value"), + unit="τ", + unit_first=True, + precision=4, + ) + + f" ({slippage_pct:.2f}%)" + ) + + emission_value = substake.emission.tao / emission_drain_tempo + emission_cell = format_cell( + emission_value, + prev.get("emission"), + unit=symbol, + unit_first=unit_first, + precision=4, + ) + + rows.append( + [ + str(netuid), # Netuid + symbol if netuid != 0 else "\u03a4", # Symbol + stake_cell, # Stake amount + rate_cell, # Rate + tao_ownership_cell, # TAO equivalent + exchange_cell, # Exchange value + swap_cell, # Swap value with slippage + "YES" + if substake.is_registered + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status + emission_cell, # Emission rate + ] + ) + + table = define_table( + hotkey_name, + rows, + total_tao_ownership, + total_tao_value, + total_swapped_tao_value, + live=True, + ) + + for row in rows: + table.add_row(*row) + + return table, current_data + + # Main execution + ( + sub_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) = await get_stake_data() + + # Iterate over substakes and aggregate them by hotkey. + hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + for substake in sub_stakes: hotkey = substake.hotkey_ss58 if substake.stake.rao == 0: @@ -2091,109 +2314,217 @@ def table_substakes(hotkey_: str, substakes: list[StakeInfo]): hotkeys_to_substakes[hotkey] = [] hotkeys_to_substakes[hotkey].append(substake) - # Iterate over each hotkey and make a table - counter = 0 - num_hotkeys = len(hotkeys_to_substakes) - all_hotkeys_total_global_tao = Balance(0) - all_hotkeys_total_tao_value = Balance(0) - for hotkey in hotkeys_to_substakes.keys(): - counter += 1 - stake, value = table_substakes(hotkey, hotkeys_to_substakes[hotkey]) - all_hotkeys_total_global_tao += stake - all_hotkeys_total_tao_value += value - - if num_hotkeys > 1 and counter < num_hotkeys: - console.print("\nPress Enter to continue to the next hotkey...") - input() - - console.print("\n\n") - console.print( - f"Wallet:\n" - f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_global_tao}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" - ) - if not sub_stakes: - console.print(f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})") - else: - display_table = Prompt.ask( - "\nPress Enter to view column descriptions or type 'q' to skip:", - choices=["", "q"], - default="", - show_choices=True, - ).lower() - - if display_table == "q": + if live: + # Select one hokkey for live monitoring + if len(hotkeys_to_substakes) > 1: console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + "\n[bold]Multiple hotkeys found. Please select one for live monitoring:[/bold]" ) - else: - header = """ - [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: - """ - console.print(header) - description_table = Table( - show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + for idx, hotkey in enumerate(hotkeys_to_substakes.keys()): + name = ( + f"{registered_delegate_info[hotkey].display} ({hotkey})" + if hotkey in registered_delegate_info + else hotkey + ) + console.print(f"[{idx}] [{COLOR_PALETTE['GENERAL']['HEADER']}]{name}") + + selected_idx = Prompt.ask( + "Enter hotkey index", + choices=[str(i) for i in range(len(hotkeys_to_substakes))], ) + selected_hotkey = list(hotkeys_to_substakes.keys())[int(selected_idx)] + selected_stakes = hotkeys_to_substakes[selected_hotkey] + else: + selected_hotkey = list(hotkeys_to_substakes.keys())[0] + selected_stakes = hotkeys_to_substakes[selected_hotkey] - fields = [ - ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), - ( - "[bold tan]Symbol[/bold tan]", - "The symbol for the subnet's dynamic TAO token.", - ), - ( - "[bold tan]Stake (α)[/bold tan]", - "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#staking[/blue].", - ), - ( - "[bold tan]TAO Reserves (τ_in)[/bold tan]", - 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', - ), - ( - "[bold tan]Alpha Reserves (α_in)[/bold tan]", - "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", - ), - ( - "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", - ), - ( - "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", - ), - ( - "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', - ), - ( - "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", - ), - ( - "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", - ), - ( - "[bold tan]Registered[/bold tan]", - "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", - ), - ( - "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", - ), - ] + hotkey_name = ( + f"{registered_delegate_info[selected_hotkey].display} ({selected_hotkey})" + if selected_hotkey in registered_delegate_info + else selected_hotkey + ) + + refresh_interval = 10 # seconds + progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=20), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + console=console, + ) + progress_task = progress.add_task("Updating: ", total=refresh_interval) + + previous_block = None + current_block = None + previous_data = None + + with Live(console=console, screen=True, auto_refresh=True) as live: + try: + while True: + block_hash = await subtensor.substrate.get_chain_head() + ( + sub_stakes, + registered_delegate_info, + dynamic_info_, + emission_drain_tempo, + ) = await get_stake_data(block_hash) + selected_stakes = [ + stake + for stake in sub_stakes + if stake.hotkey_ss58 == selected_hotkey + ] - description_table.add_column( - "Field", - no_wrap=True, - style="bold tan", + dynamic_info = {info.netuid: info for info in dynamic_info_} + block_number = await subtensor.substrate.get_block_number(None) + + previous_block = current_block + current_block = block_number + new_blocks = ( + "N/A" + if previous_block is None + else str(current_block - previous_block) + ) + + table, current_data = create_live_table( + selected_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + hotkey_name, + previous_data, + ) + + previous_data = current_data + progress.reset(progress_task) + start_time = asyncio.get_event_loop().time() + + block_info = ( + f"Previous: [dark_sea_green]{previous_block}[/dark_sea_green] " + f"Current: [dark_sea_green]{current_block}[/dark_sea_green] " + f"Diff: [dark_sea_green]{new_blocks}[/dark_sea_green]" + ) + + message = f"\nLive stake view - Press [bold red]Ctrl+C[/bold red] to exit\n{block_info}" + live_render = Group(message, progress, table) + live.update(live_render) + + while not progress.finished: + await asyncio.sleep(0.1) + elapsed = asyncio.get_event_loop().time() - start_time + progress.update( + progress_task, completed=min(elapsed, refresh_interval) + ) + + except KeyboardInterrupt: + console.print("\n[bold]Stopped live updates[/bold]") + return + + else: + # Iterate over each hotkey and make a table + counter = 0 + num_hotkeys = len(hotkeys_to_substakes) + all_hotkeys_total_global_tao = Balance(0) + all_hotkeys_total_tao_value = Balance(0) + for hotkey in hotkeys_to_substakes.keys(): + counter += 1 + stake, value = create_table(hotkey, hotkeys_to_substakes[hotkey]) + all_hotkeys_total_global_tao += stake + all_hotkeys_total_tao_value += value + + if num_hotkeys > 1 and counter < num_hotkeys: + console.print("\nPress Enter to continue to the next hotkey...") + input() + + balance = await subtensor.get_balance(coldkey_address) + console.print("\n\n") + console.print( + f"Wallet:\n" + f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance[coldkey_address]}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_global_tao}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + ) + if not sub_stakes: + console.print( + f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" ) - description_table.add_column("Description", overflow="fold") - for field_name, description in fields: - description_table.add_row(field_name, description) - console.print(description_table) + else: + display_table = Prompt.ask( + "\nPress Enter to view column descriptions or type 'q' to skip:", + choices=["", "q"], + default="", + show_choices=True, + ).lower() + + if display_table == "q": + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + ) + else: + header = """ + [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Stake (α)[/bold tan]", + "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#staking[/blue].", + ), + ( + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', + ), + ( + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", + ), + ( + "[bold tan]Alpha out (α_out)[/bold tan]", + "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", + ), + ( + "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', + ), + ( + "[bold tan]Exchange Value (α x τ/α)[/bold tan]", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", + ), + ( + "[bold tan]Swap (α → τ)[/bold tan]", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", + ), + ( + "[bold tan]Registered[/bold tan]", + "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Emission (α/block)[/bold tan]", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", + ), + ] + + description_table.add_column( + "Field", + no_wrap=True, + style="bold tan", + ) + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) async def move_stake( From 85727a0a90519c6eec8057863c29fe59f41403db Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 30 Dec 2024 12:08:30 -0800 Subject: [PATCH 158/332] s list, s show working --- bittensor_cli/src/bittensor/chain_data.py | 24 ++++--- bittensor_cli/src/commands/subnets.py | 84 ++++------------------- 2 files changed, 26 insertions(+), 82 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 9465edda..7eacc096 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1215,9 +1215,9 @@ class SubnetState: trust: list[float] rank: list[float] block_at_registration: list[int] - local_stake: list[Balance] - global_stake: list[Balance] - stake_weight: list[float] + alpha_stake: list[Balance] + tao_stake: list[Balance] + total_stake: list[Balance] emission_history: list[list[int]] @classmethod @@ -1261,13 +1261,15 @@ def fix_decoded_values(cls, decoded: dict) -> "SubnetState": trust=[u16_normalized_float(val) for val in decoded["trust"]], rank=[u16_normalized_float(val) for val in decoded["rank"]], block_at_registration=decoded["block_at_registration"], - local_stake=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["local_stake"] + alpha_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["alpha_stake"] ], - global_stake=[ - Balance.from_rao(val).set_unit(0) for val in decoded["global_stake"] + tao_stake=[ + Balance.from_rao(val).set_unit(0) for val in decoded["tao_stake"] + ], + total_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["total_stake"] ], - stake_weight=[u16_normalized_float(val) for val in decoded["stake_weight"]], emission_history=decoded["emission_history"], ) @@ -1471,9 +1473,9 @@ def decode(result: list[int]) -> list[dict]: ["trust", "Vec>"], ["rank", "Vec>"], ["block_at_registration", "Vec>"], - ["local_stake", "Vec>"], - ["global_stake", "Vec>"], - ["stake_weight", "Vec>"], + ["alpha_stake", "Vec>"], + ["tao_stake", "Vec>"], + ["total_stake", "Vec>"], ["emission_history", "Vec>>"], ], }, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 39050dfa..1e21157c 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -175,11 +175,8 @@ async def subnets_list( async def fetch_subnet_data(): subnets = await subtensor.get_all_subnet_dynamic_info() - global_weights, identities = await asyncio.gather( - subtensor.get_global_weights([subnet.netuid for subnet in subnets]), - subtensor.query_all_identities(), - ) - return subnets, global_weights, identities + identities = await subtensor.query_all_identities() + return subnets, identities def define_table(total_emissions: float, total_rate: float, total_netuids: int): table = Table( @@ -235,17 +232,13 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): justify="left", overflow="fold", ) - table.add_column( - "[bold white]Local weight coeff. (γ)", style="steel_blue", justify="left" - ) return table # Non-live mode - def create_table(subnets, global_weights, identities): + def create_table(subnets, identities): rows = [] for subnet in subnets: netuid = subnet.netuid - global_weight = global_weights.get(netuid) symbol = f"{subnet.symbol}\u200e" if netuid == 0: @@ -265,9 +258,6 @@ def create_table(subnets, global_weights, identities): alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" - global_weight_cell = ( - f"{global_weight:.4f}" if global_weight is not None else "N/A" - ) rows.append( ( @@ -280,7 +270,6 @@ def create_table(subnets, global_weights, identities): alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out tempo_cell, # Tempo k/n - global_weight_cell,# Local weight coeff. (γ) ) ) @@ -303,7 +292,7 @@ def create_table(subnets, global_weights, identities): return table # Live mode - def create_table_live(subnets, global_weights, identities, previous_data): + def create_table_live(subnets, identities, previous_data): def format_cell(value, previous_value, unit="", unit_first=False, precision=4): if previous_value is not None: change = value - previous_value @@ -326,7 +315,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): for subnet in subnets: netuid = subnet.netuid - global_weight = global_weights.get(netuid) symbol = f"{subnet.symbol}\u200e" if netuid == 0: @@ -344,7 +332,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "alpha_in": subnet.alpha_in.tao, "price": subnet.price.tao, "blocks_since_last_step": subnet.blocks_since_last_step, - "global_weight": global_weight, } prev = previous_data.get(netuid) if previous_data else {} @@ -404,29 +391,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" ) - # Local weight coeff cell - prev_global_weight = prev.get("global_weight") - if prev_global_weight is not None and global_weight is not None: - weight_change = float(global_weight) - float(prev_global_weight) - if weight_change > 0: - weight_change_text = ( - f" [pale_green3](+{weight_change:.6f})[/pale_green3]" - ) - elif weight_change < 0: - weight_change_text = ( - f" [hot_pink3]({weight_change:.6f})[/hot_pink3]" - ) - else: - weight_change_text = "" - else: - weight_change_text = "" - - global_weight_cell = ( - f"{global_weight:.4f}{weight_change_text}" - if global_weight is not None - else "N/A" - ) - rows.append( ( netuid_cell, # Netuid @@ -438,7 +402,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out tempo_cell, # Tempo k/n - global_weight_cell,# Local weight coeff. (γ) ) ) @@ -480,8 +443,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): try: while True: subnets = await subtensor.get_all_subnet_dynamic_info() - global_weights, identities, block_number = await asyncio.gather( - subtensor.get_global_weights([subnet.netuid for subnet in subnets]), + identities, block_number = await asyncio.gather( subtensor.query_all_identities(), subtensor.substrate.get_block_number(None) ) @@ -492,7 +454,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) table, current_data = create_table_live( - subnets, global_weights, identities, previous_data + subnets, identities, previous_data ) previous_data = current_data progress.reset(progress_task) @@ -518,8 +480,8 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): pass # Ctrl + C else: # Non-live mode - subnets, global_weights, identities = await fetch_subnet_data() - table = create_table(subnets, global_weights, identities) + subnets, identities = await fetch_subnet_data() + table = create_table(subnets, identities) console.print(table) display_table = Prompt.ask( @@ -819,9 +781,7 @@ async def show_subnet(netuid_: int): ] ) tao_sum = Balance(0) - stake_sum = Balance(0) relative_emissions_sum = 0 - stake_weight_sum = 0 for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( @@ -830,9 +790,7 @@ async def show_subnet(netuid_: int): else 0 ) relative_emissions_sum += hotkey_block_emission - tao_sum += subnet_state.global_stake[idx] - stake_sum += subnet_state.local_stake[idx] - stake_weight_sum += subnet_state.stake_weight[idx] + tao_sum += subnet_state.total_stake[idx] # Get identity for this uid coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get("name", "") @@ -842,9 +800,7 @@ async def show_subnet(netuid_: int): rows.append( ( str(idx), # UID - str(subnet_state.global_stake[idx]), # TAO - f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake - f"{subnet_state.stake_weight[idx]:.4f}", # Weight + f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends str(subnet_state.incentives[idx]), # Incentive @@ -866,25 +822,11 @@ async def show_subnet(netuid_: int): # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( - f"TAO({Balance.get_unit(0)})", - style=COLOR_PALETTE["STAKE"]["TAO"], - no_wrap=True, - justify="right", - footer=str(tao_sum), - ) - table.add_column( - f"Stake({Balance.get_unit(netuid_)})", + f"Stake ({Balance.get_unit(netuid_)})", style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", - footer=f"{stake_sum.set_unit(subnet_info.netuid)}", - ) - table.add_column( - f"Weight({Balance.get_unit(0)}•{Balance.get_unit(netuid_)})", - style="blue", - no_wrap=True, - justify="center", - footer=f"{stake_weight_sum:.3f}", + footer=f"{tao_sum.set_unit(subnet_info.netuid)}", ) table.add_column( "Dividends", @@ -953,7 +895,7 @@ async def show_subnet(netuid_: int): f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) console.print( From 93ad3e5153f6c1b14d0c3af6238d939e33d94d16 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 30 Dec 2024 13:18:16 -0800 Subject: [PATCH 159/332] s show --netuid 0 --- bittensor_cli/src/commands/subnets.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 1e21157c..a6fda83b 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -597,12 +597,7 @@ async def show_root(): # table.add_column("#", style="cyan", justify="right") table.add_column("[bold white]Position", style="white", justify="center") table.add_column( - f"[bold white] TAO ({Balance.get_unit(0)})", - style=COLOR_PALETTE["STAKE"]["TAO"], - justify="center", - ) - table.add_column( - f"[bold white]Stake ({Balance.get_unit(0)})", + f"[bold white]Total Stake ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="center", ) @@ -629,7 +624,7 @@ async def show_root(): sorted_hotkeys = sorted( enumerate(root_state.hotkeys), - key=lambda x: root_state.global_stake[x[0]], + key=lambda x: root_state.total_stake[x[0]], reverse=True, ) sorted_rows = [] @@ -652,13 +647,12 @@ async def show_root(): sorted_rows.append( ( - str((pos + 1)), - str(root_state.global_stake[idx]), - str(root_state.local_stake[idx]), - f"{(total_emission_per_block)}", - f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", - f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", - validator_identity, + str((pos + 1)), # Position + str(root_state.total_stake[idx]), # Total Stake + f"{(total_emission_per_block)}", # Emission + f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", # Hotkey + f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", # Coldkey + validator_identity, # Identity ) ) sorted_hks_delegation.append(root_state.hotkeys[idx]) From 7321a444fae15b32632e183485162ae1b7917fa3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 30 Dec 2024 17:25:47 -0800 Subject: [PATCH 160/332] Stake working, minor fixes --- bittensor_cli/src/bittensor/chain_data.py | 2 +- .../src/bittensor/subtensor_interface.py | 32 ++++ bittensor_cli/src/commands/stake/stake.py | 167 ++++++++++-------- bittensor_cli/src/commands/subnets.py | 58 ++++-- 4 files changed, 175 insertions(+), 84 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 7eacc096..0b3b35eb 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -897,7 +897,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": if alpha_in.tao > 0 else Balance.from_tao(1) ) - is_dynamic = True if decoded["alpha_in"] > 0 else False + is_dynamic = True if int(decoded["netuid"]) > 0 else False # TODO: Patching this temporarily return DynamicInfo( owner=ss58_encode(decoded["owner"], SS58_FORMAT), netuid=netuid, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7bd01d42..d4229a3a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1412,3 +1412,35 @@ async def get_global_weights( return { netuid: u64_normalized_float(weight) for (netuid, weight) in result.items() } + + async def get_subnet_tao( + self, netuid: Optional[int] = None, block_hash: Optional[str] = None + ) -> dict[int, Balance]: + """ + Retrieves the total TAO for one or all subnets. + + Args: + netuid: Optional specific netuid to query. If None, returns data for all subnets. + block_hash: Optional block hash to query at. + + Returns: + Dictionary mapping netuid to its total TAO balance + """ + if netuid is not None: + result = await self.substrate.query( + module="SubtensorModule", + storage_function="SubnetTAO", + params=[netuid], + block_hash=block_hash + ) + return {netuid: Balance.from_rao(result or 0)} + else: + results = await self.substrate.query_map( + module="SubtensorModule", + storage_function="SubnetTAO", + block_hash=block_hash + ) + return { + netuid: Balance.from_rao(tao or 0) + async for netuid, tao in results + } \ No newline at end of file diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b22a7513..ce608482 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -928,20 +928,28 @@ async def stake_add( hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] starting_chain_head = await subtensor.substrate.get_chain_head() - all_dynamic_info, initial_stake_balances = await asyncio.gather( + all_dynamic_info, stake_info_dict = await asyncio.gather( asyncio.gather( *[ subtensor.get_subnet_dynamic_info(x, starting_chain_head) for x in netuids ] ), - subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( - hotkey_ss58s=[x[1] for x in hotkeys_to_stake_to], - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuids=netuids, + subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], block_hash=starting_chain_head, ), ) + initial_stake_balances = {} + for hotkey_ss58 in [x[1] for x in hotkeys_to_stake_to]: + initial_stake_balances[hotkey_ss58] = {} + for netuid in netuids: + initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) + + for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + if stake_info.hotkey_ss58 in initial_stake_balances: + initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = stake_info.stake + for hk_name, hk_ss58 in hotkeys_to_stake_to: if not is_valid_ss58_address(hk_ss58): print_error( @@ -999,18 +1007,18 @@ async def stake_add( else 0 ) slippage_pct = f"{slippage_pct_float:.4f} %" + rate = str(1 / (float(dynamic_info.price) or 1)) else: slippage_pct_float = 0 slippage_pct = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" + rate = str(1) max_slippage = max(slippage_pct_float, max_slippage) rows.append( - ( - str(netuid), + ( str(netuid), # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", f"{hotkey[1]}", str(amount_to_stake_as_balance), - str(1 / (float(dynamic_info.price) or 1)) - + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", str(received_amount.set_unit(netuid)), str(slippage_pct), ) @@ -1102,16 +1110,20 @@ async def send_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: - new_balance_, new_stake_ = await asyncio.gather( + new_balance_, stake_info_dict = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=netuid_i, + subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], ), ) new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake = new_stake_.set_unit(netuid_i) + new_stake = Balance.from_rao(0) + for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + if (stake_info.hotkey_ss58 == staking_address_ss58 and + stake_info.netuid == netuid_i): + new_stake = stake_info.stake.set_unit(netuid_i) + break + console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) @@ -1664,14 +1676,20 @@ async def unstake( # Fetch stake balances chain_head = await subtensor.substrate.get_chain_head() - stake_in_netuids = ( - await subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( - hotkey_ss58s=[hk[1] for hk in hotkeys_to_unstake_from], - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuids=netuids, - block_hash=chain_head, - ) + stake_info_dict = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + block_hash=chain_head, ) + stake_in_netuids = {} + for _, stake_info_list in stake_info_dict.items(): + hotkey_stakes = {} + for stake_info in stake_info_list: + if stake_info.hotkey_ss58 not in hotkey_stakes: + hotkey_stakes[stake_info.hotkey_ss58] = {} + hotkey_stakes[stake_info.hotkey_ss58][stake_info.netuid] = stake_info.stake + + stake_in_netuids = hotkey_stakes + # Flag to check if user wants to quit skip_remaining_subnets = False @@ -1899,13 +1917,15 @@ async def unstake( wallet.coldkeypub.ss58_address ) new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=staking_address_ss58, - netuid=netuid_i, - ) - ).set_unit(netuid_i) + new_stake_info = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + ) + new_stake = Balance.from_rao(0) + for stake_info in new_stake_info[wallet.coldkeypub.ss58_address]: + if (stake_info.hotkey_ss58 == staking_address_ss58 and + stake_info.netuid == netuid_i): + new_stake = stake_info.stake.set_unit(netuid_i) + break console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) @@ -1931,23 +1951,23 @@ async def get_stake_data(block_hash: str = None): substakes, registered_delegate_info, dynamic_info, - emission_drain_tempo, + # emission_drain_tempo, ) = await asyncio.gather( subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[coldkey_address], block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.get_all_subnet_dynamic_info(), - subtensor.substrate.query( - "SubtensorModule", "HotkeyEmissionTempo", block_hash=block_hash - ), + # subtensor.substrate.query( + # "SubtensorModule", "HotkeyEmissionTempo", block_hash=block_hash + # ), ) sub_stakes = substakes[coldkey_address] return ( sub_stakes, registered_delegate_info, dynamic_info, - emission_drain_tempo, + # emission_drain_tempo, ) def define_table( @@ -2083,8 +2103,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): issuance = pool.alpha_out if pool.is_dynamic else tao_locked # Per block emission cell - per_block_emission = substake_.emission.tao / emission_drain_tempo - + per_block_emission = substake_.emission.tao / pool.tempo # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: if issuance.tao != 0: @@ -2132,7 +2151,7 @@ def create_live_table( substakes: list, registered_delegate_info: dict, dynamic_info: dict, - emission_drain_tempo: int, + # emission_drain_tempo: int, hotkey_name: str, previous_data: Optional[dict] = None, ) -> tuple[Table, dict, Balance, Balance, Balance]: @@ -2196,7 +2215,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "price": pool.price.tao, "tao_value": tao_value.tao, "swapped_value": swapped_tao_value.tao, - "emission": substake.emission.tao / emission_drain_tempo, + "emission": substake.emission.tao / pool.tempo, "tao_ownership": tao_ownership.tao, } @@ -2256,7 +2275,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + f" ({slippage_pct:.2f}%)" ) - emission_value = substake.emission.tao / emission_drain_tempo + emission_value = substake.emission.tao / pool.tempo emission_cell = format_cell( emission_value, prev.get("emission"), @@ -2300,7 +2319,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): sub_stakes, registered_delegate_info, dynamic_info, - emission_drain_tempo, + # emission_drain_tempo, ) = await get_stake_data() # Iterate over substakes and aggregate them by hotkey. @@ -2365,7 +2384,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): sub_stakes, registered_delegate_info, dynamic_info_, - emission_drain_tempo, + # emission_drain_tempo, ) = await get_stake_data(block_hash) selected_stakes = [ stake @@ -2388,7 +2407,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): selected_stakes, registered_delegate_info, dynamic_info, - emission_drain_tempo, + # emission_drain_tempo, hotkey_name, previous_data, ) @@ -2539,21 +2558,26 @@ async def move_stake( ): origin_hotkey_ss58 = wallet.hotkey.ss58_address # Get the wallet stake balances. - origin_stake_balance: Balance = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey_ss58, - netuid=origin_netuid, - ) - ).set_unit(origin_netuid) - - destination_stake_balance: Balance = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=destination_hotkey, - netuid=destination_netuid, - ) - ).set_unit(destination_netuid) + origin_stake_balance = Balance.from_rao(0) + destination_stake_balance = Balance.from_rao(0) + + chain_head = await subtensor.substrate.get_chain_head() + stake_info_dict = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + block_hash=chain_head, + ) + + for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + if (stake_info.hotkey_ss58 == origin_hotkey_ss58 and + stake_info.netuid == origin_netuid): + origin_stake_balance = stake_info.stake + elif (stake_info.hotkey_ss58 == destination_hotkey and + stake_info.netuid == destination_netuid): + destination_stake_balance = stake_info.stake + + # Set appropriate units + origin_stake_balance = origin_stake_balance.set_unit(origin_netuid) + destination_stake_balance = destination_stake_balance.set_unit(destination_netuid) if origin_stake_balance == Balance.from_tao(0).set_unit(origin_netuid): print_error( @@ -2744,20 +2768,21 @@ async def move_stake( ) return else: - new_origin_stake_balance: Balance = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey_ss58, - netuid=origin_netuid, - ) - ).set_unit(origin_netuid) - new_destination_stake_balance: Balance = ( - await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=destination_hotkey, - netuid=destination_netuid, - ) - ).set_unit(destination_netuid) + new_stake_info_dict = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + ) + + new_origin_stake_balance = Balance.from_rao(0) + new_destination_stake_balance = Balance.from_rao(0) + + for stake_info in new_stake_info_dict[wallet.coldkeypub.ss58_address]: + if (stake_info.hotkey_ss58 == origin_hotkey_ss58 and + stake_info.netuid == origin_netuid): + new_origin_stake_balance = stake_info.stake.set_unit(origin_netuid) + elif (stake_info.hotkey_ss58 == destination_hotkey and + stake_info.netuid == destination_netuid): + new_destination_stake_balance = stake_info.stake.set_unit(destination_netuid) + console.print( f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index a6fda83b..931afba3 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -174,9 +174,13 @@ async def subnets_list( """List all subnet netuids in the network.""" async def fetch_subnet_data(): - subnets = await subtensor.get_all_subnet_dynamic_info() - identities = await subtensor.query_all_identities() - return subnets, identities + block_number = await subtensor.substrate.get_block_number(None) + subnets, identities, subnet_tao = await asyncio.gather( + subtensor.get_all_subnet_dynamic_info(), + subtensor.query_all_identities(), + subtensor.get_subnet_tao(), + ) + return subnets, identities, subnet_tao, block_number def define_table(total_emissions: float, total_rate: float, total_netuids: int): table = Table( @@ -226,6 +230,11 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) + table.add_column( + "[bold white]Total TAO/Current block (Temp)", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="left", + ) table.add_column( "[bold white]Tempo (k/n)", style=COLOR_PALETTE["GENERAL"]["TEMPO"], @@ -235,11 +244,17 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): return table # Non-live mode - def create_table(subnets, identities): + def create_table(subnets, identities, subnet_tao, block_number): rows = [] + total_tao = Balance(0) for subnet in subnets: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" + if netuid == 0: + subnet_tao_balance = subnet_tao.get(netuid, Balance(0)) + else: + subnet_tao_balance = sum(subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0) + total_tao = subnet_tao_balance if netuid == 0: emission_tao = 0.0 @@ -257,6 +272,7 @@ def create_table(subnets, identities): tao_in_cell = f"τ {subnet.tao_in.tao:,.4f}" alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" + subnet_tao_cell = f"τ {subnet_tao_balance.tao:,.4f} / {block_number}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" rows.append( @@ -269,6 +285,7 @@ def create_table(subnets, identities): tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out + subnet_tao_cell, # Subnet TAO tempo_cell, # Tempo k/n ) ) @@ -292,7 +309,7 @@ def create_table(subnets, identities): return table # Live mode - def create_table_live(subnets, identities, previous_data): + def create_table_live(subnets, identities, previous_data, subnet_tao, block_number): def format_cell(value, previous_value, unit="", unit_first=False, precision=4): if previous_value is not None: change = value - previous_value @@ -323,6 +340,12 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): else: emission_tao = subnet.emission.tao identity = identities.get(subnet.owner, {}).get("name", "~") + + if netuid == 0: + subnet_tao_balance = subnet_tao.get(netuid, Balance(0)) + else: + subnet_tao_balance = sum(subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0) + # Store current values for comparison current_data[netuid] = { @@ -332,6 +355,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "alpha_in": subnet.alpha_in.tao, "price": subnet.price.tao, "blocks_since_last_step": subnet.blocks_since_last_step, + "subnet_tao": subnet_tao_balance.tao, } prev = previous_data.get(netuid) if previous_data else {} @@ -367,6 +391,14 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): precision=5, ) + subnet_tao_cell = format_cell( + subnet_tao_balance.tao, + prev.get("subnet_tao"), + unit="τ", + unit_first=True, + precision=4 + ) + f" / {block_number}" + # Tempo cell prev_blocks_since_last_step = prev.get("blocks_since_last_step") if prev_blocks_since_last_step is not None: @@ -401,6 +433,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out + subnet_tao_cell, # Subnet TAO tempo_cell, # Tempo k/n ) ) @@ -443,9 +476,10 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): try: while True: subnets = await subtensor.get_all_subnet_dynamic_info() - identities, block_number = await asyncio.gather( + identities, block_number, subnet_tao = await asyncio.gather( subtensor.query_all_identities(), - subtensor.substrate.get_block_number(None) + subtensor.substrate.get_block_number(None), + subtensor.get_subnet_tao() ) # Update block numbers @@ -454,7 +488,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) table, current_data = create_table_live( - subnets, identities, previous_data + subnets, identities, previous_data, subnet_tao, block_number ) previous_data = current_data progress.reset(progress_task) @@ -480,8 +514,8 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): pass # Ctrl + C else: # Non-live mode - subnets, identities = await fetch_subnet_data() - table = create_table(subnets, identities) + subnets, identities, subnet_tao, block_number = await fetch_subnet_data() + table = create_table(subnets, identities, subnet_tao, block_number) console.print(table) display_table = Prompt.ask( @@ -707,7 +741,7 @@ async def show_root(): if 1 <= idx <= max_rows: selected_hotkey = sorted_hks_delegation[idx - 1] row_data = sorted_rows[idx - 1] - identity = row_data[6] + identity = "" if row_data[5] == "~" else row_data[5] identity_str = f" ({identity})" if identity else "" console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}") @@ -925,7 +959,7 @@ async def show_subnet(netuid_: int): uid = int(sorted_rows[idx-1][0]) hotkey = subnet_state.hotkeys[uid] row_data = sorted_rows[idx-1] - identity = row_data[9] + identity = "" if row_data[7] == "~" else row_data[7] identity_str = f" ({identity})" if identity else "" console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}") return hotkey From 4f08d1ae881f923b97c87dd0ba96bc43e29c4e3b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 31 Dec 2024 10:57:25 -0800 Subject: [PATCH 161/332] Adds tao emission % to footer --- bittensor_cli/src/commands/subnets.py | 72 +++++++++++---------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 931afba3..5bd0acc5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -181,8 +181,21 @@ async def fetch_subnet_data(): subtensor.get_subnet_tao(), ) return subnets, identities, subnet_tao, block_number + + def calculate_emission_stats(subnet_tao: dict, block_number: int) -> tuple[Balance, str]: + # We do not include the root subnet in the emission calculation + total_tao_emitted = sum( + subnet_tao.get(n, Balance(0)) + for n in subnet_tao.keys() + if n != 0 + ) + emission_percentage = (total_tao_emitted.tao / block_number) * 100 + percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" + formatted_percentage = f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" + percentage_string = f"{total_tao_emitted:}/{block_number} ({formatted_percentage})" + return total_tao_emitted, percentage_string - def define_table(total_emissions: float, total_rate: float, total_netuids: int): + def define_table(total_emissions: float, total_rate: float, total_netuids: int, tao_emission_percentage: str): table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", @@ -219,6 +232,7 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", + footer=f"{tao_emission_percentage}", ) table.add_column( f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", @@ -230,11 +244,6 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) - table.add_column( - "[bold white]Total TAO/Current block (Temp)", - style=COLOR_PALETTE["STAKE"]["TAO"], - justify="left", - ) table.add_column( "[bold white]Tempo (k/n)", style=COLOR_PALETTE["GENERAL"]["TEMPO"], @@ -245,16 +254,12 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int): # Non-live mode def create_table(subnets, identities, subnet_tao, block_number): - rows = [] - total_tao = Balance(0) + rows = [] + _, percentage_string = calculate_emission_stats(subnet_tao, block_number) + for subnet in subnets: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" - if netuid == 0: - subnet_tao_balance = subnet_tao.get(netuid, Balance(0)) - else: - subnet_tao_balance = sum(subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0) - total_tao = subnet_tao_balance if netuid == 0: emission_tao = 0.0 @@ -272,7 +277,6 @@ def create_table(subnets, identities, subnet_tao, block_number): tao_in_cell = f"τ {subnet.tao_in.tao:,.4f}" alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" - subnet_tao_cell = f"τ {subnet_tao_balance.tao:,.4f} / {block_number}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" rows.append( @@ -285,7 +289,6 @@ def create_table(subnets, identities, subnet_tao, block_number): tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out - subnet_tao_cell, # Subnet TAO tempo_cell, # Tempo k/n ) ) @@ -297,7 +300,7 @@ def create_table(subnets, identities, subnet_tao, block_number): float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) total_netuids = len(subnets) - table = define_table(total_emissions, total_rate, total_netuids) + table = define_table(total_emissions, total_rate, total_netuids, percentage_string) # Sort rows by stake, keeping the root subnet in the first position sorted_rows = [rows[0]] + sorted( @@ -329,6 +332,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): rows = [] current_data = {} # To store current values for comparison in the next update + _, percentage_string = calculate_emission_stats(subnet_tao, block_number) for subnet in subnets: netuid = subnet.netuid @@ -340,13 +344,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): else: emission_tao = subnet.emission.tao identity = identities.get(subnet.owner, {}).get("name", "~") - - if netuid == 0: - subnet_tao_balance = subnet_tao.get(netuid, Balance(0)) - else: - subnet_tao_balance = sum(subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0) - - + # Store current values for comparison current_data[netuid] = { "emission_tao": emission_tao, @@ -355,7 +353,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "alpha_in": subnet.alpha_in.tao, "price": subnet.price.tao, "blocks_since_last_step": subnet.blocks_since_last_step, - "subnet_tao": subnet_tao_balance.tao, } prev = previous_data.get(netuid) if previous_data else {} @@ -391,14 +388,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): precision=5, ) - subnet_tao_cell = format_cell( - subnet_tao_balance.tao, - prev.get("subnet_tao"), - unit="τ", - unit_first=True, - precision=4 - ) + f" / {block_number}" - # Tempo cell prev_blocks_since_last_step = prev.get("blocks_since_last_step") if prev_blocks_since_last_step is not None: @@ -433,7 +422,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): tao_in_cell, # TAO Pool τ_in alpha_in_cell, # Alpha Pool α_in alpha_out_cell, # Stake α_out - subnet_tao_cell, # Subnet TAO tempo_cell, # Tempo k/n ) ) @@ -445,7 +433,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) total_netuids = len(subnets) - table = define_table(total_emissions, total_rate, total_netuids) + table = define_table(total_emissions, total_rate, total_netuids, percentage_string) # Sort rows by stake, keeping the first subnet in the first position sorted_rows = [rows[0]] + sorted( @@ -548,16 +536,16 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "Shows how the one τ per block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's alpha token price by the sum of all alpha prices across all the subnets. This fraction of TAO is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", ), ( - "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", - ), - ( - "[bold tan]TAO Reserves (τ_in)[/bold tan]", + "[bold tan]TAO Pool (τ_in)[/bold tan]", 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', ), ( - "[bold tan]Alpha Reserves (α_in)[/bold tan]", + "[bold tan]Alpha Pool (α_in)[/bold tan]", "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + ), + ( + "[bold tan]STAKE (α_out)[/bold tan]", + "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", @@ -567,10 +555,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): "[bold tan]Tempo (k/n)[/bold tan]", 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#tempo-kn[/blue].', ), - ( - "[bold tan]Local weight coeff (γ)[/bold tan]", - "This is the global_split coefficient. It is a multiplication factor between 0 and 1, and it controls the balance between a validator's normalized global and local weights. In effect, the global_split parameter controls the balance between the validator hotkey's local and global influence. This is a subnet parameter. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#global-split[/blue].", - ), ] description_table.add_column("Field", no_wrap=True, style="bold tan") From 8a878fbde8248e6a2ca51e2e1508cd1afb725bc7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 31 Dec 2024 11:23:22 -0800 Subject: [PATCH 162/332] Cleanup --- bittensor_cli/src/commands/stake/stake.py | 45 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index ce608482..f313cf11 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1951,23 +1951,18 @@ async def get_stake_data(block_hash: str = None): substakes, registered_delegate_info, dynamic_info, - # emission_drain_tempo, ) = await asyncio.gather( subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[coldkey_address], block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.get_all_subnet_dynamic_info(), - # subtensor.substrate.query( - # "SubtensorModule", "HotkeyEmissionTempo", block_hash=block_hash - # ), ) sub_stakes = substakes[coldkey_address] return ( sub_stakes, registered_delegate_info, dynamic_info, - # emission_drain_tempo, ) def define_table( @@ -2151,7 +2146,6 @@ def create_live_table( substakes: list, registered_delegate_info: dict, dynamic_info: dict, - # emission_drain_tempo: int, hotkey_name: str, previous_data: Optional[dict] = None, ) -> tuple[Table, dict, Balance, Balance, Balance]: @@ -2319,7 +2313,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): sub_stakes, registered_delegate_info, dynamic_info, - # emission_drain_tempo, ) = await get_stake_data() # Iterate over substakes and aggregate them by hotkey. @@ -2384,7 +2377,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): sub_stakes, registered_delegate_info, dynamic_info_, - # emission_drain_tempo, ) = await get_stake_data(block_hash) selected_stakes = [ stake @@ -2407,7 +2399,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): selected_stakes, registered_delegate_info, dynamic_info, - # emission_drain_tempo, hotkey_name, previous_data, ) @@ -2799,3 +2790,39 @@ async def fetch_coldkey_stake(subtensor: "SubtensorInterface", wallet: Wallet): coldkey_ss58=wallet.coldkeypub.ss58_address ) return sub_stakes + + +# TODO: Use this in all subnet commands. +async def get_stake_info_for_coldkey_and_hotkey( + subtensor: "SubtensorInterface", + coldkey_ss58: str, + hotkey_ss58: Optional[str] = None, + netuid: Optional[int] = None, + block_hash: Optional[str] = None +) -> dict[tuple[str, int], Balance]: + """Helper function to get stake info for a coldkey and optionally filter by hotkey and netuid. + + Args: + subtensor: SubtensorInterface instance + coldkey_ss58: Coldkey SS58 address + hotkey_ss58: Optional hotkey SS58 address to filter by + netuid: Optional netuid to filter by + block_hash: Optional block hash to query at + + Returns: + Dictionary mapping (hotkey, netuid) tuple to stake balance + """ + stake_info_dict = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[coldkey_ss58], + block_hash=block_hash + ) + + stakes = {} + for stake_info in stake_info_dict[coldkey_ss58]: + if hotkey_ss58 and stake_info.hotkey_ss58 != hotkey_ss58: + continue + if netuid is not None and stake_info.netuid != netuid: + continue + stakes[(stake_info.hotkey_ss58, stake_info.netuid)] = stake_info.stake + + return stakes From fdc4cb3d8fb9b15b2012db00dd8b3c2bffa5ea64 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 31 Dec 2024 11:45:41 -0800 Subject: [PATCH 163/332] Bumps version - testing deploy --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index c3c1da00..2e57f5b6 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0+rao.1" +__version__ = "8.2.0rc1rao" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1781830f..ea285bbe 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -61,7 +61,7 @@ class GitError(Exception): pass -__version__ = "8.2.0+rao.1" +__version__ = "8.2.0rc1rao" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 2a1f24597fd3ea8f2d3d55031f7a85f577cb6dbd Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 31 Dec 2024 11:51:10 -0800 Subject: [PATCH 164/332] Bumps version (again) --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 2e57f5b6..f5baec6f 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc1rao" +__version__ = "8.2.0rc1" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ea285bbe..f72db482 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -61,7 +61,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc1rao" +__version__ = "8.2.0rc1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 278a1d696fcd1d248a180a8f2444aa38048ce2dc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 2 Jan 2025 13:46:36 -0800 Subject: [PATCH 165/332] Revamps readability of numbers --- bittensor_cli/cli.py | 5 +- bittensor_cli/src/bittensor/utils.py | 26 ++++++++ bittensor_cli/src/commands/stake/stake.py | 38 +++++++---- bittensor_cli/src/commands/subnets.py | 80 +++++++++++++---------- 4 files changed, 101 insertions(+), 48 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f72db482..7aa4de58 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2500,7 +2500,9 @@ def stake_list( ) return self._run_command( - stake.stake_list(wallet, coldkey_ss58, self.initialize_chain(network), live) + stake.stake_list( + wallet, coldkey_ss58, self.initialize_chain(network), live, verbose + ) ) def stake_add( @@ -3549,6 +3551,7 @@ def subnets_list( False, # reuse-last False, # html-output not self.config.get("use_cache", True), + verbose, live_mode, ) ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 7987db00..c261ddf4 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -645,6 +645,32 @@ def millify(n: int): return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx]) +def millify_tao(n: float, start_at: str = "K") -> str: + """ + Dupe of millify, but for ease in converting tao values. + Allows thresholds to be specified for different suffixes. + """ + mill_names = ["", "k", "m", "b", "t"] + thresholds = {"K": 1, "M": 2, "B": 3, "T": 4} + + if start_at not in thresholds: + raise ValueError(f"start_at must be one of {list(thresholds.keys())}") + + n_ = float(n) + if n_ == 0: + return "0.00" + + mill_idx = int(math.floor(math.log10(abs(n_)) / 3)) + + # Number's index is below our threshold, return with commas + if mill_idx < thresholds[start_at]: + return f"{n_:,.2f}" + + mill_idx = max(thresholds[start_at], min(len(mill_names) - 1, mill_idx)) + + return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx]) + + def normalize_hyperparameters( subnet: "SubnetHyperparameters", ) -> list[tuple[str, str, str]]: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index f313cf11..8ae63499 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -28,6 +28,7 @@ u16_normalized_float, format_error_message, group_subnets, + millify_tao ) if TYPE_CHECKING: @@ -1943,6 +1944,7 @@ async def stake_list( coldkey_ss58: str, subtensor: "SubtensorInterface", live: bool = False, + verbose: bool = False, ): coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address @@ -2015,21 +2017,21 @@ def define_table( f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], justify="right", - footer=f"{total_tao_ownership}", + footer=f"τ {millify_tao(total_tao_ownership.tao)}" if not verbose else f"{total_tao_ownership}", ) table.add_column( f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", footer_style="overline white", style=COLOR_PALETTE["STAKE"]["TAO"], justify="right", - footer=f"{total_tao_value}", + footer=f"τ {millify_tao(total_tao_value.tao)}" if not verbose else f"{total_tao_value}", ) table.add_column( f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", footer_style="overline white", style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], justify="right", - footer=f"{total_swapped_tao_value}", + footer=f"τ {millify_tao(total_swapped_tao_value.tao)}" if not verbose else f"{total_swapped_tao_value}", ) table.add_column( "[white]Registered", @@ -2114,17 +2116,16 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): alpha_ownership = "0.0000" tao_ownership = "0.0000" + stake_value = millify_tao(substake_.stake.tao) if not verbose else f"{substake_.stake.tao:,.4f}" rows.append( [ str(netuid), # Number symbol if netuid != 0 else "\u03a4", # Symbol - f"{substake_.stake.tao:,.4f} {symbol}" - if netuid != 0 - else f"{symbol} {substake_.stake.tao:,.4f}", # Stake (a) + f"{stake_value} {symbol}" if netuid != 0 else f"{symbol} {stake_value}", # Stake (a) f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) - f"{tao_ownership}", # TAO equiv - f"{tao_value}", # Exchange Value (α x τ/α) - f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ + f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv + f"τ {millify_tao(tao_value.tao)}" if not verbose else f"{tao_value}", # Exchange Value (α x τ/α) + f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" if not verbose else f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ "YES" if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered @@ -2156,10 +2157,11 @@ def create_live_table( total_tao_value = Balance(0) total_swapped_tao_value = Balance(0) - def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): if previous_value is not None: change = value - previous_value if abs(change) > 10 ** (-precision): + formatted_change = f"{change:.{precision}f}" if not millify else f"{millify_tao(change)}" change_text = ( f" [pale_green3](+{change:.{precision}f})[/pale_green3]" if change > 0 @@ -2169,10 +2171,11 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): change_text = "" else: change_text = "" + formatted_value = f"{value:,.{precision}f}" if not millify else f"{millify_tao(value)}" return ( - f"{value:,.{precision}f} {unit}{change_text}" + f"{formatted_value} {unit}{change_text}" if not unit_first - else f"{unit} {value:,.{precision}f}{change_text}" + else f"{unit} {formatted_value}{change_text}" ) # Process each stake @@ -2223,6 +2226,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit=symbol, unit_first=unit_first, precision=4, + millify=True if not verbose else False, ) rate_cell = format_cell( @@ -2239,6 +2243,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit="τ", unit_first=True, precision=4, + millify=True if not verbose else False, ) exchange_cell = format_cell( @@ -2247,6 +2252,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit="τ", unit_first=True, precision=4, + millify=True if not verbose else False, ) if pool.is_dynamic: @@ -2265,6 +2271,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit="τ", unit_first=True, precision=4, + millify=True if not verbose else False, ) + f" ({slippage_pct:.2f}%)" ) @@ -2445,13 +2452,16 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): input() balance = await subtensor.get_balance(coldkey_address) + total_tao_value = f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}" if not verbose else all_hotkeys_total_tao_value + total_tao_ownership = f"τ {millify_tao(all_hotkeys_total_global_tao.tao)}" if not verbose else all_hotkeys_total_global_tao + console.print("\n\n") console.print( f"Wallet:\n" f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance[coldkey_address]}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_global_tao}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_ownership}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) if not sub_stakes: console.print( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 5bd0acc5..5596d159 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -34,7 +34,7 @@ print_error, format_error_message, get_metadata_table, - millify, + millify_tao, render_table, update_metadata_table, prompt_for_identity, @@ -169,6 +169,7 @@ async def subnets_list( reuse_last: bool, html_output: bool, no_cache: bool, + verbose: bool, live: bool, ): """List all subnet netuids in the network.""" @@ -180,7 +181,16 @@ async def fetch_subnet_data(): subtensor.query_all_identities(), subtensor.get_subnet_tao(), ) - return subnets, identities, subnet_tao, block_number + + # Sort subnets by stake, keeping the root subnet in the first position + root_subnet = next(s for s in subnets if s.netuid == 0) + other_subnets = sorted( + [s for s in subnets if s.netuid != 0], + key=lambda x: x.alpha_out.tao, + reverse=True + ) + sorted_subnets = [root_subnet] + other_subnets + return sorted_subnets, identities, subnet_tao, block_number def calculate_emission_stats(subnet_tao: dict, block_number: int) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation @@ -192,7 +202,10 @@ def calculate_emission_stats(subnet_tao: dict, block_number: int) -> tuple[Balan emission_percentage = (total_tao_emitted.tao / block_number) * 100 percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" formatted_percentage = f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" - percentage_string = f"{total_tao_emitted:}/{block_number} ({formatted_percentage})" + if not verbose: + percentage_string = f"τ {millify_tao(total_tao_emitted.tao)}/{millify_tao(block_number)} ({formatted_percentage})" + else: + percentage_string = f"τ {total_tao_emitted.tao:,.1f}/{block_number} ({formatted_percentage})" return total_tao_emitted, percentage_string def define_table(total_emissions: float, total_rate: float, total_netuids: int, tao_emission_percentage: str): @@ -267,6 +280,9 @@ def create_table(subnets, identities, subnet_tao, block_number): else: emission_tao = subnet.emission.tao identity = identities.get(subnet.owner, {}).get("name", "~") + + alpha_in_value = f"{millify_tao(subnet.alpha_in.tao)}" if not verbose else f"{subnet.alpha_in.tao:,.4f}" + alpha_out_value = f"{millify_tao(subnet.alpha_out.tao)}" if not verbose else f"{subnet.alpha_out.tao:,.4f}" # Prepare cells netuid_cell = str(netuid) @@ -274,9 +290,9 @@ def create_table(subnets, identities, subnet_tao, block_number): subnet_name_cell = SUBNETS.get(netuid, "~") emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" - tao_in_cell = f"τ {subnet.tao_in.tao:,.4f}" - alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" - alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" + tao_in_cell = f"τ {millify_tao(subnet.tao_in.tao)}" if not verbose else f"τ {subnet.tao_in.tao:,.4f}" + alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_in_value}" + alpha_out_cell = f"{alpha_out_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_out_value}" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" rows.append( @@ -302,18 +318,13 @@ def create_table(subnets, identities, subnet_tao, block_number): total_netuids = len(subnets) table = define_table(total_emissions, total_rate, total_netuids, percentage_string) - # Sort rows by stake, keeping the root subnet in the first position - sorted_rows = [rows[0]] + sorted( - rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True - ) - - for row in sorted_rows: + for row in rows: table.add_row(*row) return table # Live mode def create_table_live(subnets, identities, previous_data, subnet_tao, block_number): - def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): if previous_value is not None: change = value - previous_value if change > 0.01: @@ -328,7 +339,8 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): change_text = "" else: change_text = "" - return f"{value:,.{precision}f} {unit}{change_text}" if not unit_first else f"{unit} {value:,.{precision}f}{change_text}" + formatted_value = f"{value:,.{precision}f}" if not millify else millify_tao(value) + return f"{formatted_value} {unit}{change_text}" if not unit_first else f"{unit} {formatted_value}{change_text}" rows = [] current_data = {} # To store current values for comparison in the next update @@ -371,7 +383,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4 ) tao_in_cell = format_cell( - subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4 + subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4, millify=True if not verbose else False ) alpha_in_cell = format_cell( subnet.alpha_in.tao, @@ -379,6 +391,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit=f"{symbol}", unit_first=unit_first, precision=4, + millify=True if not verbose else False, ) alpha_out_cell = format_cell( subnet.alpha_out.tao, @@ -386,6 +399,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): unit=f"{symbol}", unit_first=unit_first, precision=5, + millify=True if not verbose else False, ) # Tempo cell @@ -435,11 +449,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4): total_netuids = len(subnets) table = define_table(total_emissions, total_rate, total_netuids, percentage_string) - # Sort rows by stake, keeping the first subnet in the first position - sorted_rows = [rows[0]] + sorted( - rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True - ) - for row in sorted_rows: + for row in rows: table.add_row(*row) return table, current_data @@ -611,8 +621,7 @@ async def show_root(): show_lines=False, pad_edge=True, ) - # if delegate_selection: - # table.add_column("#", style="cyan", justify="right") + table.add_column("[bold white]Position", style="white", justify="center") table.add_column( f"[bold white]Total Stake ({Balance.get_unit(0)})", @@ -666,10 +675,10 @@ async def show_root(): sorted_rows.append( ( str((pos + 1)), # Position - str(root_state.total_stake[idx]), # Total Stake - f"{(total_emission_per_block)}", # Emission - f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", # Hotkey - f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", # Coldkey + f"τ {millify_tao(root_state.total_stake[idx].tao)}" if verbose else f"{root_state.total_stake[idx]}", # Total Stake + f"{total_emission_per_block}", # Emission + f"{root_state.hotkeys[idx][:6]}" if verbose else f"{root_state.hotkeys[idx]}", # Hotkey + f"{root_state.coldkeys[idx][:6]}" if verbose else f"{root_state.coldkeys[idx]}", # Coldkey validator_identity, # Identity ) ) @@ -688,13 +697,16 @@ async def show_root(): console.print("\n") if not delegate_selection: + tao_pool = f"{millify_tao(root_info.tao_in.tao)}" if not verbose else f"{root_info.tao_in.tao:,.4f}" + alpha_pool = f"{millify_tao(root_info.alpha_in.tao)}" if not verbose else f"{root_info.alpha_in.tao:,.4f}" + stake = f"{millify_tao(root_info.alpha_out.tao)}" if not verbose else f"{root_info.alpha_out.tao:,.5f}" console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {root_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol}{root_info.alpha_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {root_info.alpha_out.tao:,.5f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {alpha_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {stake}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) console.print( @@ -812,7 +824,7 @@ async def show_subnet(netuid_: int): rows.append( ( str(idx), # UID - f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake + f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends str(subnet_state.incentives[idx]), # Incentive @@ -838,7 +850,7 @@ async def show_subnet(netuid_: int): style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", - footer=f"{tao_sum.set_unit(subnet_info.netuid)}", + footer=f"{tao_sum.set_unit(subnet_info.netuid)}" if verbose else f"{millify_tao(tao_sum.tao)} {subnet_info.symbol}", ) table.add_column( "Dividends", @@ -899,14 +911,16 @@ async def show_subnet(netuid_: int): if not delegate_selection: subnet_name = SUBNETS.get(netuid_, '') subnet_name_display = f": {subnet_name}" if subnet_name else "" + tao_pool = f"{millify_tao(subnet_info.tao_in.tao)}" if not verbose else f"{subnet_info.tao_in.tao:,.4f}" + alpha_pool = f"{millify_tao(subnet_info.alpha_in.tao)}" if not verbose else f"{subnet_info.alpha_in.tao:,.4f}" console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) From cfb5343e07ff189ba7a07492df7730b9799ee65b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 09:38:56 -0800 Subject: [PATCH 166/332] Updates docstrings --- bittensor_cli/src/commands/stake/stake.py | 18 +++++++++--------- bittensor_cli/src/commands/subnets.py | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 8ae63499..2950171c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2496,35 +2496,35 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m ), ( "[bold tan]Stake (α)[/bold tan]", - "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#staking[/blue].", + "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#staking[/blue].", ), ( "[bold tan]TAO Reserves (τ_in)[/bold tan]", - 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].', ), ( "[bold tan]Alpha Reserves (α_in)[/bold tan]", - "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", + "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", ), ( "[bold tan]Alpha out (α_out)[/bold tan]", - "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", + "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", ), ( "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", - 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', ), ( "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", ), ( "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", ), ( "[bold tan]Registered[/bold tan]", @@ -2532,7 +2532,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m ), ( "[bold tan]Emission (α/block)[/bold tan]", - "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#emissions[/blue].", ), ] diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 5596d159..8b77a53a 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -543,27 +543,27 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m ), ( "[bold tan]Emission (τ)[/bold tan]", - "Shows how the one τ per block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's alpha token price by the sum of all alpha prices across all the subnets. This fraction of TAO is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", + "Shows how the one τ per block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's alpha token price by the sum of all alpha prices across all the subnets. This fraction of TAO is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#emissions[/blue].", ), ( "[bold tan]TAO Pool (τ_in)[/bold tan]", - 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].', ), ( "[bold tan]Alpha Pool (α_in)[/bold tan]", - "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].", ), ( "[bold tan]STAKE (α_out)[/bold tan]", - "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", + "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - 'Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].', + 'Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].', ), ( "[bold tan]Tempo (k/n)[/bold tan]", - 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#tempo-kn[/blue].', + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#tempo-kn[/blue].', ), ] From 00d4aa1791f1e24cb8547841cb0bb6e3fe4a9f60 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 09:55:19 -0800 Subject: [PATCH 167/332] Updates repr of rates --- bittensor_cli/src/commands/stake/stake.py | 4 ++-- bittensor_cli/src/commands/subnets.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2950171c..3c3554cc 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2114,7 +2114,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else: # TODO what's this var for? alpha_ownership = "0.0000" - tao_ownership = "0.0000" + tao_ownership = Balance.from_tao(0) stake_value = millify_tao(substake_.stake.tao) if not verbose else f"{substake_.stake.tao:,.4f}" rows.append( @@ -2122,7 +2122,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): str(netuid), # Number symbol if netuid != 0 else "\u03a4", # Symbol f"{stake_value} {symbol}" if netuid != 0 else f"{symbol} {stake_value}", # Stake (a) - f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) + f"{millify_tao(pool.price.tao)} τ/{symbol}" if not verbose else f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv f"τ {millify_tao(tao_value.tao)}" if not verbose else f"{tao_value}", # Exchange Value (α x τ/α) f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" if not verbose else f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 8b77a53a..5bf6a4f7 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -380,7 +380,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m emission_tao, prev.get("emission_tao"), unit="τ", unit_first=True, precision=4 ) price_cell = format_cell( - subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4 + subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4, millify=True if not verbose else False ) tao_in_cell = format_cell( subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4, millify=True if not verbose else False From f88519c20216bea68f32084069eed4a516bb852e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 10:08:20 -0800 Subject: [PATCH 168/332] Updates s show netuid 0 --- bittensor_cli/src/commands/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 5bf6a4f7..37736855 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -675,7 +675,7 @@ async def show_root(): sorted_rows.append( ( str((pos + 1)), # Position - f"τ {millify_tao(root_state.total_stake[idx].tao)}" if verbose else f"{root_state.total_stake[idx]}", # Total Stake + f"τ {millify_tao(root_state.total_stake[idx].tao)}" if not verbose else f"{root_state.total_stake[idx]}", # Total Stake f"{total_emission_per_block}", # Emission f"{root_state.hotkeys[idx][:6]}" if verbose else f"{root_state.hotkeys[idx]}", # Hotkey f"{root_state.coldkeys[idx][:6]}" if verbose else f"{root_state.coldkeys[idx]}", # Coldkey From cc71fb30bd73a4db27fc10db6edabeca13bca2a9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 10:28:11 -0800 Subject: [PATCH 169/332] Temp columns in s show --- bittensor_cli/src/commands/subnets.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 37736855..ff25ef42 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -825,6 +825,8 @@ async def show_subnet(netuid_: int): ( str(idx), # UID f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake + f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake + f"τ {subnet_state.tao_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(subnet_state.tao_stake[idx])}", # Tao Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends str(subnet_state.incentives[idx]), # Incentive @@ -852,6 +854,20 @@ async def show_subnet(netuid_: int): justify="right", footer=f"{tao_sum.set_unit(subnet_info.netuid)}" if verbose else f"{millify_tao(tao_sum.tao)} {subnet_info.symbol}", ) + # ------- Temporary columns for testing ------- + table.add_column( + f"Alpha Stake ({Balance.get_unit(netuid_)}) TST", + style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + no_wrap=True, + justify="right", + ) + table.add_column( + f"Tao Stake (τ) TST", + style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + no_wrap=True, + justify="right", + ) + # ------- End Temporary columns for testing ------- table.add_column( "Dividends", style=COLOR_PALETTE["POOLS"]["EMISSION"], From 83fe739f26621749aa1d374c7168410a4f57471a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 10:42:03 -0800 Subject: [PATCH 170/332] Change column width --- bittensor_cli/src/commands/subnets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index ff25ef42..ff7fd5d5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -856,13 +856,13 @@ async def show_subnet(netuid_: int): ) # ------- Temporary columns for testing ------- table.add_column( - f"Alpha Stake ({Balance.get_unit(netuid_)}) TST", + f"Alpha({Balance.get_unit(netuid_)})", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", ) table.add_column( - f"Tao Stake (τ) TST", + f"Tao(τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", From f50e89e58b024555f8d427d6bac4f055bb5fe3c3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 11:15:19 -0800 Subject: [PATCH 171/332] Adds owner highlighting --- .../src/bittensor/subtensor_interface.py | 36 +++++++++++++++---- bittensor_cli/src/commands/subnets.py | 10 ++++-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d4229a3a..03c7f7ed 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -920,7 +920,7 @@ async def fetch_coldkey_hotkey_identities( "identity": coldkey_identity, "hotkeys": hotkeys, } - + for hotkey_ss58 in hotkeys: identities["hotkeys"][hotkey_ss58] = { "coldkey": coldkey_ss58, @@ -1431,16 +1431,38 @@ async def get_subnet_tao( module="SubtensorModule", storage_function="SubnetTAO", params=[netuid], - block_hash=block_hash + block_hash=block_hash, ) return {netuid: Balance.from_rao(result or 0)} else: results = await self.substrate.query_map( module="SubtensorModule", storage_function="SubnetTAO", - block_hash=block_hash + block_hash=block_hash, ) - return { - netuid: Balance.from_rao(tao or 0) - async for netuid, tao in results - } \ No newline at end of file + return {netuid: Balance.from_rao(tao or 0) async for netuid, tao in results} + + async def get_owned_hotkeys( + self, + coldkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + :param coldkey_ss58: The SS58 address of the coldkey to query. + :param block_hash: The hash of the blockchain block number for the query. + :param reuse_block: Whether to reuse the last-used blockchain block hash. + + :return: A list of hotkey SS58 addresses owned by the coldkey. + """ + owned_hotkeys = await self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index ff7fd5d5..eda36cd8 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -806,7 +806,8 @@ async def show_subnet(netuid_: int): ) tao_sum = Balance(0) relative_emissions_sum = 0 - + owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner) + for idx, hk in enumerate(subnet_state.hotkeys): hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum @@ -820,6 +821,9 @@ async def show_subnet(netuid_: int): coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get("name", "") hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) uid_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "~") + + if subnet_state.coldkeys[idx] == subnet_info.owner or subnet_state.hotkeys[idx] in owner_hotkeys: + uid_identity += " [orange3](*Owner)[/orange3]" rows.append( ( @@ -856,13 +860,13 @@ async def show_subnet(netuid_: int): ) # ------- Temporary columns for testing ------- table.add_column( - f"Alpha({Balance.get_unit(netuid_)})", + f"Alpha ({Balance.get_unit(netuid_)})", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", ) table.add_column( - f"Tao(τ)", + f"Tao (τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", From 10de2eae4bff876a3f6d940d33dd4c0006f65ec6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 11:34:08 -0800 Subject: [PATCH 172/332] Adds test columns to s show root + millify stuff --- bittensor_cli/src/commands/subnets.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index eda36cd8..43e81251 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -628,6 +628,20 @@ async def show_root(): style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="center", ) + # ------- Temporary columns for testing ------- + table.add_column( + "Alpha (τ)", + style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + no_wrap=True, + justify="right", + ) + table.add_column( + "Tao (τ)", + style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + no_wrap=True, + justify="right", + ) + # ------- End Temporary columns for testing ------- table.add_column( f"[bold white]Emission ({Balance.get_unit(0)}/block)", style=COLOR_PALETTE["POOLS"]["EMISSION"], @@ -676,9 +690,11 @@ async def show_root(): ( str((pos + 1)), # Position f"τ {millify_tao(root_state.total_stake[idx].tao)}" if not verbose else f"{root_state.total_stake[idx]}", # Total Stake + f"τ {root_state.alpha_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(root_state.alpha_stake[idx])}", # Alpha Stake + f"τ {root_state.tao_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(root_state.tao_stake[idx])}", # Tao Stake f"{total_emission_per_block}", # Emission - f"{root_state.hotkeys[idx][:6]}" if verbose else f"{root_state.hotkeys[idx]}", # Hotkey - f"{root_state.coldkeys[idx][:6]}" if verbose else f"{root_state.coldkeys[idx]}", # Coldkey + f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", # Hotkey + f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", # Coldkey validator_identity, # Identity ) ) @@ -700,9 +716,10 @@ async def show_root(): tao_pool = f"{millify_tao(root_info.tao_in.tao)}" if not verbose else f"{root_info.tao_in.tao:,.4f}" alpha_pool = f"{millify_tao(root_info.alpha_in.tao)}" if not verbose else f"{root_info.alpha_in.tao:,.4f}" stake = f"{millify_tao(root_info.alpha_out.tao)}" if not verbose else f"{root_info.alpha_out.tao:,.5f}" + rate = f"{millify_tao(root_info.price.tao)}" if not verbose else f"{root_info.price.tao:,.4f}" console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{rate} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {alpha_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" From 51457907fafff07386a9ffb0ea017eaea4ece853 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 11:53:34 -0800 Subject: [PATCH 173/332] Formats live mode changes --- bittensor_cli/src/commands/stake/stake.py | 5 +++-- bittensor_cli/src/commands/subnets.py | 11 +++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 3c3554cc..1b898ced 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2163,9 +2163,9 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m if abs(change) > 10 ** (-precision): formatted_change = f"{change:.{precision}f}" if not millify else f"{millify_tao(change)}" change_text = ( - f" [pale_green3](+{change:.{precision}f})[/pale_green3]" + f" [pale_green3](+{formatted_change})[/pale_green3]" if change > 0 - else f" [hot_pink3]({change:.{precision}f})[/hot_pink3]" + else f" [hot_pink3]({formatted_change})[/hot_pink3]" ) else: change_text = "" @@ -2235,6 +2235,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m unit=f"τ/{symbol}", unit_first=False, precision=5, + millify=True if not verbose else False, ) tao_ownership_cell = format_cell( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 43e81251..f20be569 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -327,13 +327,12 @@ def create_table_live(subnets, identities, previous_data, subnet_tao, block_numb def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): if previous_value is not None: change = value - previous_value - if change > 0.01: + if abs(change) > 10 ** (-precision): + formatted_change = f"{change:.{precision}f}" if not millify else f"{millify_tao(change)}" change_text = ( - f" [pale_green3](+{change:.2f})[/pale_green3]" - ) - elif change < -0.01: - change_text = ( - f" [hot_pink3]({change:.2f})[/hot_pink3]" + f" [pale_green3](+{formatted_change})[/pale_green3]" + if change > 0 + else f" [hot_pink3]({formatted_change})[/hot_pink3]" ) else: change_text = "" From 135c4acedc729e92e46a3415d389d777ff1b2583 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 13:07:52 -0800 Subject: [PATCH 174/332] Formatting + reactivated w balance --- bittensor_cli/cli.py | 8 ++++---- bittensor_cli/src/commands/subnets.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7aa4de58..a200978a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1483,11 +1483,11 @@ def wallet_transfer( validate=WV.WALLET, ) - # For Rao games + # For Rao games - temporarilyt commented out effective_network = get_effective_network(self.config, network) - if is_rao_network(effective_network): - print_error("This command is disabled on the 'rao' network.") - raise typer.Exit() + # if is_rao_network(effective_network): + # print_error("This command is disabled on the 'rao' network.") + # raise typer.Exit() subtensor = self.initialize_chain(network) return self._run_command( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index f20be569..7f917d68 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -283,13 +283,14 @@ def create_table(subnets, identities, subnet_tao, block_number): alpha_in_value = f"{millify_tao(subnet.alpha_in.tao)}" if not verbose else f"{subnet.alpha_in.tao:,.4f}" alpha_out_value = f"{millify_tao(subnet.alpha_out.tao)}" if not verbose else f"{subnet.alpha_out.tao:,.4f}" + price_value = f"{millify_tao(subnet.price.tao)}" if not verbose else f"{subnet.price.tao:,.4f}" # Prepare cells netuid_cell = str(netuid) symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" subnet_name_cell = SUBNETS.get(netuid, "~") emission_cell = f"τ {emission_tao:,.4f}" - price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" + price_cell = f"{price_value} τ/{symbol}" tao_in_cell = f"τ {millify_tao(subnet.tao_in.tao)}" if not verbose else f"τ {subnet.tao_in.tao:,.4f}" alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_in_value}" alpha_out_cell = f"{alpha_out_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_out_value}" @@ -839,7 +840,7 @@ async def show_subnet(netuid_: int): uid_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "~") if subnet_state.coldkeys[idx] == subnet_info.owner or subnet_state.hotkeys[idx] in owner_hotkeys: - uid_identity += " [orange3](*Owner)[/orange3]" + uid_identity = f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" rows.append( ( From bfdf89f25893a017a8f240bc437d0dd11ee80670 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 13:22:05 -0800 Subject: [PATCH 175/332] fmt totals --- bittensor_cli/src/commands/subnets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 7f917d68..bd987018 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -446,6 +446,8 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m total_rate = sum( float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) + total_emissions = f"{millify_tao(total_emissions)}" if not verbose else f"{total_emissions:,.2f}" + total_rate = f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" total_netuids = len(subnets) table = define_table(total_emissions, total_rate, total_netuids, percentage_string) From 253fbad1152fc1e6e552473c5d73bf53d120ee28 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 14:06:27 -0800 Subject: [PATCH 176/332] Fmt --- bittensor_cli/src/commands/subnets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index bd987018..f7df7dd3 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -233,13 +233,13 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="#AB7CC8", justify="left", - footer=f"τ {total_rate:.4f}", + footer=f"τ {total_rate}", ) table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="left", - footer=f"τ {total_emissions:.4f}", + footer=f"τ {total_emissions}", ) table.add_column( f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", From 71d9793e18ea80e5bbca94a84ed98b6c1610e94b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 17:35:10 -0800 Subject: [PATCH 177/332] Revamps s list --- bittensor_cli/src/bittensor/chain_data.py | 7 +- bittensor_cli/src/commands/subnets.py | 185 +++++++++++++++------- 2 files changed, 128 insertions(+), 64 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 0b3b35eb..01b5c7f3 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -893,11 +893,12 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": total_locked = Balance.from_rao(decoded["total_locked"]).set_unit(netuid) owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit(netuid) price = ( - Balance.from_tao(tao_in.tao / alpha_in.tao) + Balance.from_tao(1.0) if netuid == 0 + else Balance.from_tao(tao_in.tao / alpha_in.tao) if alpha_in.tao > 0 else Balance.from_tao(1) - ) - is_dynamic = True if int(decoded["netuid"]) > 0 else False # TODO: Patching this temporarily + ) # TODO: Patching this temporarily for netuid 0 + is_dynamic = True if int(decoded["netuid"]) > 0 else False # TODO: Patching this temporarily for netuid 0 return DynamicInfo( owner=ss58_encode(decoded["owner"], SS58_FORMAT), netuid=netuid, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index f7df7dd3..c06e2eb5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -176,21 +176,20 @@ async def subnets_list( async def fetch_subnet_data(): block_number = await subtensor.substrate.get_block_number(None) - subnets, identities, subnet_tao = await asyncio.gather( + subnets, subnet_tao = await asyncio.gather( subtensor.get_all_subnet_dynamic_info(), - subtensor.query_all_identities(), subtensor.get_subnet_tao(), ) - # Sort subnets by stake, keeping the root subnet in the first position + # Sort subnets by market cap, keeping the root subnet in the first position root_subnet = next(s for s in subnets if s.netuid == 0) other_subnets = sorted( [s for s in subnets if s.netuid != 0], - key=lambda x: x.alpha_out.tao, + key=lambda x: (x.alpha_in.tao + x.alpha_out.tao) * x.price.tao, reverse=True ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, identities, subnet_tao, block_number + return sorted_subnets, subnet_tao, block_number def calculate_emission_stats(subnet_tao: dict, block_number: int) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation @@ -223,18 +222,18 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, ) table.add_column("[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids)) - table.add_column( - "[bold white]Symbol", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], - justify="right", - ) table.add_column("[bold white]Name", style="cyan", justify="left") table.add_column( - f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", - style="#AB7CC8", + f"[bold white]Price \n({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style="dark_sea_green2", justify="left", footer=f"τ {total_rate}", ) + table.add_column( + f"[bold white]Market Cap \n({Balance.get_unit(1)} * Price)", + style="steel_blue3", + justify="left", + ) table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], @@ -242,21 +241,22 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, footer=f"τ {total_emissions}", ) table.add_column( - f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", + f"[bold white]Liquidity \n({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", footer=f"{tao_emission_percentage}", ) table.add_column( - f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + f"[bold white]Stake ({Balance.get_unit(1)}_out)", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) table.add_column( - f"[bold white]Stake ({Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + f"[bold white]Supply ({Balance.get_unit(1)})", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="left", ) + table.add_column( "[bold white]Tempo (k/n)", style=COLOR_PALETTE["GENERAL"]["TEMPO"], @@ -266,7 +266,7 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, return table # Non-live mode - def create_table(subnets, identities, subnet_tao, block_number): + def create_table(subnets, subnet_tao, block_number): rows = [] _, percentage_string = calculate_emission_stats(subnet_tao, block_number) @@ -276,36 +276,46 @@ def create_table(subnets, identities, subnet_tao, block_number): if netuid == 0: emission_tao = 0.0 - identity = "~" else: emission_tao = subnet.emission.tao - identity = identities.get(subnet.owner, {}).get("name", "~") alpha_in_value = f"{millify_tao(subnet.alpha_in.tao)}" if not verbose else f"{subnet.alpha_in.tao:,.4f}" alpha_out_value = f"{millify_tao(subnet.alpha_out.tao)}" if not verbose else f"{subnet.alpha_out.tao:,.4f}" price_value = f"{millify_tao(subnet.price.tao)}" if not verbose else f"{subnet.price.tao:,.4f}" + + # Market Cap + market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao + market_cap_value = f"{millify_tao(market_cap)}" if not verbose else f"{market_cap:,.4f}" + + # Liquidity + tao_in_cell = f"τ {millify_tao(subnet.tao_in.tao)}" if not verbose else f"τ {subnet.tao_in.tao:,.4f}" + alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_in_value}" + + # Supply + supply = subnet.alpha_in.tao + subnet.alpha_out.tao + supply_value = f"{millify_tao(supply)}" if not verbose else f"{supply:,.4f}" # Prepare cells netuid_cell = str(netuid) - symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" - subnet_name_cell = SUBNETS.get(netuid, "~") + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{price_value} τ/{symbol}" - tao_in_cell = f"τ {millify_tao(subnet.tao_in.tao)}" if not verbose else f"τ {subnet.tao_in.tao:,.4f}" - alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_in_value}" + liquidity_cell = f"{tao_in_cell}, {alpha_in_cell}" alpha_out_cell = f"{alpha_out_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_out_value}" + market_cap_cell = f"τ {market_cap_value}" + supply_cell = f"{supply_value} {symbol} [#806DAF]/21M" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" rows.append( ( netuid_cell, # Netuid - symbol_cell, # Symbol subnet_name_cell, # Name price_cell, # Rate τ_in/α_in + market_cap_cell, # Market Cap emission_cell, # Emission (τ) - tao_in_cell, # TAO Pool τ_in - alpha_in_cell, # Alpha Pool α_in + liquidity_cell, # Liquidity (t_in, a_in) alpha_out_cell, # Stake α_out + supply_cell, # Supply tempo_cell, # Tempo k/n ) ) @@ -324,7 +334,7 @@ def create_table(subnets, identities, subnet_tao, block_number): return table # Live mode - def create_table_live(subnets, identities, previous_data, subnet_tao, block_number): + def create_table_live(subnets, previous_data, subnet_tao, block_number): def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): if previous_value is not None: change = value - previous_value @@ -342,6 +352,38 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m formatted_value = f"{value:,.{precision}f}" if not millify else millify_tao(value) return f"{formatted_value} {unit}{change_text}" if not unit_first else f"{unit} {formatted_value}{change_text}" + def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, precision=4, millify=False, netuid=None): + """Format liquidity cell with combined changes""" + + tao_str = f"τ {millify_tao(tao_val)}" if millify else f"τ {tao_val:,.{precision}f}" + _alpha_str = f"{millify_tao(alpha_val) if millify else f'{alpha_val:,.{precision}f}'}" + alpha_str = f"{_alpha_str} {symbol}" if netuid != 0 else f"{symbol} {_alpha_str}" + + # Show delta + if prev_tao is not None and prev_alpha is not None: + tao_change = tao_val - prev_tao + alpha_change = alpha_val - prev_alpha + + # Show changes if either value changed + if abs(tao_change) > 10**(-precision) or abs(alpha_change) > 10**(-precision): + + if millify: + tao_change_str = f"+{millify_tao(tao_change)}" if tao_change > 0 else f"{millify_tao(tao_change)}" + alpha_change_str = f"+{millify_tao(alpha_change)}" if alpha_change > 0 else f"{millify_tao(alpha_change)}" + else: + tao_change_str = f"+{tao_change:.{precision}f}" if tao_change > 0 else f"{tao_change:.{precision}f}" + alpha_change_str = f"+{alpha_change:.{precision}f}" if alpha_change > 0 else f"{alpha_change:.{precision}f}" + + changes_str = f" [pale_green3]({tao_change_str}[/pale_green3]" if tao_change > 0 else \ + f" [hot_pink3]({tao_change_str}[/hot_pink3]" if tao_change < 0 else \ + f" [white]({tao_change_str}[/white]" + changes_str += f"[pale_green3],{alpha_change_str})[/pale_green3]" if alpha_change > 0 else \ + f"[hot_pink3],{alpha_change_str})[/hot_pink3]" if alpha_change < 0 else \ + f"[white],{alpha_change_str})[/white]" + return f"{tao_str}, {alpha_str}{changes_str}" + + return f"{tao_str}, {alpha_str}" + rows = [] current_data = {} # To store current values for comparison in the next update _, percentage_string = calculate_emission_stats(subnet_tao, block_number) @@ -352,47 +394,40 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m if netuid == 0: emission_tao = 0.0 - identity = "~" else: emission_tao = subnet.emission.tao - identity = identities.get(subnet.owner, {}).get("name", "~") - + + market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao + supply = subnet.alpha_in.tao + subnet.alpha_out.tao + # Store current values for comparison current_data[netuid] = { + "market_cap": market_cap, "emission_tao": emission_tao, "alpha_out": subnet.alpha_out.tao, "tao_in": subnet.tao_in.tao, "alpha_in": subnet.alpha_in.tao, "price": subnet.price.tao, + "supply": supply, "blocks_since_last_step": subnet.blocks_since_last_step, } prev = previous_data.get(netuid) if previous_data else {} # Prepare cells - netuid_cell = str(netuid) - symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" - subnet_name_cell = SUBNETS.get(netuid, "~") if netuid == 0: unit_first = True else: unit_first = False + + netuid_cell = str(netuid) + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" emission_cell = format_cell( emission_tao, prev.get("emission_tao"), unit="τ", unit_first=True, precision=4 ) price_cell = format_cell( subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4, millify=True if not verbose else False ) - tao_in_cell = format_cell( - subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4, millify=True if not verbose else False - ) - alpha_in_cell = format_cell( - subnet.alpha_in.tao, - prev.get("alpha_in"), - unit=f"{symbol}", - unit_first=unit_first, - precision=4, - millify=True if not verbose else False, - ) + alpha_out_cell = format_cell( subnet.alpha_out.tao, prev.get("alpha_out"), @@ -401,6 +436,35 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m precision=5, millify=True if not verbose else False, ) + liquidity_cell = format_liquidity_cell( + subnet.tao_in.tao, + subnet.alpha_in.tao, + prev.get("tao_in"), + prev.get("alpha_in"), + symbol, + precision=4, + millify=not verbose, + netuid=netuid, + ) + + market_cap_cell = format_cell( + market_cap, + prev.get("market_cap"), + unit="τ", + unit_first=True, + precision=4, + millify=True if not verbose else False, + ) + + # Supply cell + supply_cell = format_cell( + supply, + prev.get("supply"), + unit=f"{symbol} [#806DAF]/21M", + unit_first=False, + precision=2, + millify=True if not verbose else False, + ) # Tempo cell prev_blocks_since_last_step = prev.get("blocks_since_last_step") @@ -429,26 +493,28 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m rows.append( ( netuid_cell, # Netuid - symbol_cell, # Symbol subnet_name_cell, # Name price_cell, # Rate τ_in/α_in + market_cap_cell, # Market Cap emission_cell, # Emission (τ) - tao_in_cell, # TAO Pool τ_in - alpha_in_cell, # Alpha Pool α_in + liquidity_cell, # Liquidity (t_in, a_in) alpha_out_cell, # Stake α_out + supply_cell, # Supply tempo_cell, # Tempo k/n ) ) - - total_emissions = sum( + + # Calculate totals + total_netuids = len(subnets) + _total_emissions = sum( float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) + total_emissions = f"{millify_tao(_total_emissions)}" if not verbose else f"{_total_emissions:,.2f}" + total_rate = sum( float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) - total_emissions = f"{millify_tao(total_emissions)}" if not verbose else f"{total_emissions:,.2f}" total_rate = f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" - total_netuids = len(subnets) table = define_table(total_emissions, total_rate, total_netuids, percentage_string) for row in rows: @@ -475,12 +541,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets = await subtensor.get_all_subnet_dynamic_info() - identities, block_number, subnet_tao = await asyncio.gather( - subtensor.query_all_identities(), - subtensor.substrate.get_block_number(None), - subtensor.get_subnet_tao() - ) + subnets, subnet_tao, block_number = await fetch_subnet_data() # Update block numbers previous_block = current_block @@ -488,7 +549,7 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) table, current_data = create_table_live( - subnets, identities, previous_data, subnet_tao, block_number + subnets, previous_data, subnet_tao, block_number ) previous_data = current_data progress.reset(progress_task) @@ -514,10 +575,12 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m pass # Ctrl + C else: # Non-live mode - subnets, identities, subnet_tao, block_number = await fetch_subnet_data() - table = create_table(subnets, identities, subnet_tao, block_number) + subnets, subnet_tao, block_number = await fetch_subnet_data() + table = create_table(subnets, subnet_tao, block_number) console.print(table) + return + # TODO: Temporarily returning till we update docs display_table = Prompt.ask( "\nPress Enter to view column descriptions or type 'q' to skip:", choices=["", "q"], From df5d9d0890e2c52c6b8a36ca18c69f96f73f59a7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 18:52:13 -0800 Subject: [PATCH 178/332] Revamped st list --- bittensor_cli/src/commands/stake/stake.py | 105 ++++++++++++---------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 1b898ced..3c11dcb2 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -14,7 +14,7 @@ from rich.live import Live from substrateinterface.exceptions import SubstrateRequestException -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE, SUBNETS from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( @@ -1996,11 +1996,18 @@ def define_table( style="grey89", ) table.add_column( - "[white]Symbol", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], - justify="center", + "[white]Name", + style="cyan", + justify="left", no_wrap=True, ) + table.add_column( + f"[white]Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="right", + footer=f"τ {millify_tao(total_tao_value.tao)}" if not verbose else f"{total_tao_value}", + ) table.add_column( f"[white]Stake ({Balance.get_unit(1)})", footer_style="overline white", @@ -2008,24 +2015,11 @@ def define_table( justify="center", ) table.add_column( - f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", + f"[white]Price \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", footer_style="white", style=COLOR_PALETTE["POOLS"]["RATE"], justify="center", ) - table.add_column( - f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - justify="right", - footer=f"τ {millify_tao(total_tao_ownership.tao)}" if not verbose else f"{total_tao_ownership}", - ) - table.add_column( - f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", - footer_style="overline white", - style=COLOR_PALETTE["STAKE"]["TAO"], - justify="right", - footer=f"τ {millify_tao(total_tao_value.tao)}" if not verbose else f"{total_tao_value}", - ) table.add_column( f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", footer_style="overline white", @@ -2055,7 +2049,14 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): total_tao_ownership = Balance(0) total_tao_value = Balance(0) total_swapped_tao_value = Balance(0) - for substake_ in substakes: + root_stakes = [s for s in substakes if s.netuid == 0] + other_stakes = sorted( + [s for s in substakes if s.netuid != 0], + key=lambda x: dynamic_info[x.netuid].alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)).tao, + reverse=True + ) + sorted_substakes = root_stakes + other_stakes + for substake_ in sorted_substakes: netuid = substake_.netuid pool = dynamic_info[netuid] symbol = f"{Balance.get_unit(netuid)}\u200e" @@ -2093,6 +2094,11 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else: slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" + if netuid == 0: + swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_percentage})" + else: + swap_value = f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" if not verbose else f"{swapped_tao_value} ({slippage_percentage})" + # TAO locked cell tao_locked = pool.tao_in @@ -2117,15 +2123,17 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): tao_ownership = Balance.from_tao(0) stake_value = millify_tao(substake_.stake.tao) if not verbose else f"{substake_.stake.tao:,.4f}" + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" + rows.append( [ str(netuid), # Number - symbol if netuid != 0 else "\u03a4", # Symbol + subnet_name_cell, # Symbol + name + f"τ {millify_tao(tao_value.tao)}" if not verbose else f"{tao_value}", # Value (α x τ/α) f"{stake_value} {symbol}" if netuid != 0 else f"{symbol} {stake_value}", # Stake (a) f"{millify_tao(pool.price.tao)} τ/{symbol}" if not verbose else f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) - f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv - f"τ {millify_tao(tao_value.tao)}" if not verbose else f"{tao_value}", # Exchange Value (α x τ/α) - f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" if not verbose else f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ + # f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv + swap_value, # Swap(α) -> τ "YES" if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered @@ -2177,9 +2185,18 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m if not unit_first else f"{unit} {formatted_value}{change_text}" ) + + # Sort subnets by value + root_stakes = [s for s in substakes if s.netuid == 0] + other_stakes = sorted( + [s for s in substakes if s.netuid != 0], + key=lambda x: dynamic_info[x.netuid].alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)).tao, + reverse=True + ) + sorted_substakes = root_stakes + other_stakes # Process each stake - for substake in substakes: + for substake in sorted_substakes: netuid = substake.netuid pool = dynamic_info.get(netuid) if substake.stake.rao == 0 or not pool: @@ -2238,15 +2255,6 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m millify=True if not verbose else False, ) - tao_ownership_cell = format_cell( - tao_ownership.tao, - prev.get("tao_ownership"), - unit="τ", - unit_first=True, - precision=4, - millify=True if not verbose else False, - ) - exchange_cell = format_cell( tao_value.tao, prev.get("tao_value"), @@ -2265,17 +2273,20 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m else: slippage_pct = 0 - swap_cell = ( - format_cell( - swapped_tao_value.tao, - prev.get("swapped_value"), - unit="τ", - unit_first=True, - precision=4, - millify=True if not verbose else False, + if netuid != 0: + swap_cell = ( + format_cell( + swapped_tao_value.tao, + prev.get("swapped_value"), + unit="τ", + unit_first=True, + precision=4, + millify=True if not verbose else False, + ) + + f" ({slippage_pct:.2f}%)" ) - + f" ({slippage_pct:.2f}%)" - ) + else: + swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_pct}%)" emission_value = substake.emission.tao / pool.tempo emission_cell = format_cell( @@ -2285,15 +2296,15 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m unit_first=unit_first, precision=4, ) + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" rows.append( [ str(netuid), # Netuid - symbol if netuid != 0 else "\u03a4", # Symbol + subnet_name_cell, + exchange_cell, # Exchange value stake_cell, # Stake amount rate_cell, # Rate - tao_ownership_cell, # TAO equivalent - exchange_cell, # Exchange value swap_cell, # Swap value with slippage "YES" if substake.is_registered @@ -2469,6 +2480,8 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" ) else: + # TODO: Temporarily returning till we update docs + return display_table = Prompt.ask( "\nPress Enter to view column descriptions or type 'q' to skip:", choices=["", "q"], From b8706cef5476cb4b326b600998b7514a91bfbd83 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 18:52:41 -0800 Subject: [PATCH 179/332] Ruff --- bittensor_cli/src/commands/stake/stake.py | 169 +++++--- bittensor_cli/src/commands/subnets.py | 448 ++++++++++++++++------ 2 files changed, 440 insertions(+), 177 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 3c11dcb2..a7b81369 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -28,7 +28,7 @@ u16_normalized_float, format_error_message, group_subnets, - millify_tao + millify_tao, ) if TYPE_CHECKING: @@ -946,11 +946,13 @@ async def stake_add( initial_stake_balances[hotkey_ss58] = {} for netuid in netuids: initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) - + for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: if stake_info.hotkey_ss58 in initial_stake_balances: - initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = stake_info.stake - + initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) + for hk_name, hk_ss58 in hotkeys_to_stake_to: if not is_valid_ss58_address(hk_ss58): print_error( @@ -1015,7 +1017,8 @@ async def stake_add( rate = str(1) max_slippage = max(slippage_pct_float, max_slippage) rows.append( - ( str(netuid), + ( + str(netuid), # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", f"{hotkey[1]}", str(amount_to_stake_as_balance), @@ -1120,8 +1123,10 @@ async def send_extrinsic( new_balance = new_balance_[wallet.coldkeypub.ss58_address] new_stake = Balance.from_rao(0) for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: - if (stake_info.hotkey_ss58 == staking_address_ss58 and - stake_info.netuid == netuid_i): + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): new_stake = stake_info.stake.set_unit(netuid_i) break @@ -1687,11 +1692,12 @@ async def unstake( for stake_info in stake_info_list: if stake_info.hotkey_ss58 not in hotkey_stakes: hotkey_stakes[stake_info.hotkey_ss58] = {} - hotkey_stakes[stake_info.hotkey_ss58][stake_info.netuid] = stake_info.stake + hotkey_stakes[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) stake_in_netuids = hotkey_stakes - # Flag to check if user wants to quit skip_remaining_subnets = False if hotkeys_to_unstake_from: @@ -1923,8 +1929,10 @@ async def unstake( ) new_stake = Balance.from_rao(0) for stake_info in new_stake_info[wallet.coldkeypub.ss58_address]: - if (stake_info.hotkey_ss58 == staking_address_ss58 and - stake_info.netuid == netuid_i): + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): new_stake = stake_info.stake.set_unit(netuid_i) break console.print( @@ -2006,7 +2014,9 @@ def define_table( footer_style="overline white", style=COLOR_PALETTE["STAKE"]["TAO"], justify="right", - footer=f"τ {millify_tao(total_tao_value.tao)}" if not verbose else f"{total_tao_value}", + footer=f"τ {millify_tao(total_tao_value.tao)}" + if not verbose + else f"{total_tao_value}", ) table.add_column( f"[white]Stake ({Balance.get_unit(1)})", @@ -2025,7 +2035,9 @@ def define_table( footer_style="overline white", style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], justify="right", - footer=f"τ {millify_tao(total_swapped_tao_value.tao)}" if not verbose else f"{total_swapped_tao_value}", + footer=f"τ {millify_tao(total_swapped_tao_value.tao)}" + if not verbose + else f"{total_swapped_tao_value}", ) table.add_column( "[white]Registered", @@ -2052,8 +2064,10 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): root_stakes = [s for s in substakes if s.netuid == 0] other_stakes = sorted( [s for s in substakes if s.netuid != 0], - key=lambda x: dynamic_info[x.netuid].alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)).tao, - reverse=True + key=lambda x: dynamic_info[x.netuid] + .alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)) + .tao, + reverse=True, ) sorted_substakes = root_stakes + other_stakes for substake_ in sorted_substakes: @@ -2097,7 +2111,11 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): if netuid == 0: swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_percentage})" else: - swap_value = f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" if not verbose else f"{swapped_tao_value} ({slippage_percentage})" + swap_value = ( + f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})" + if not verbose + else f"{swapped_tao_value} ({slippage_percentage})" + ) # TAO locked cell tao_locked = pool.tao_in @@ -2122,16 +2140,29 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): alpha_ownership = "0.0000" tao_ownership = Balance.from_tao(0) - stake_value = millify_tao(substake_.stake.tao) if not verbose else f"{substake_.stake.tao:,.4f}" - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" - + stake_value = ( + millify_tao(substake_.stake.tao) + if not verbose + else f"{substake_.stake.tao:,.4f}" + ) + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + + f" {SUBNETS.get(netuid, '~')}" + ) + rows.append( [ str(netuid), # Number subnet_name_cell, # Symbol + name - f"τ {millify_tao(tao_value.tao)}" if not verbose else f"{tao_value}", # Value (α x τ/α) - f"{stake_value} {symbol}" if netuid != 0 else f"{symbol} {stake_value}", # Stake (a) - f"{millify_tao(pool.price.tao)} τ/{symbol}" if not verbose else f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) + f"τ {millify_tao(tao_value.tao)}" + if not verbose + else f"{tao_value}", # Value (α x τ/α) + f"{stake_value} {symbol}" + if netuid != 0 + else f"{symbol} {stake_value}", # Stake (a) + f"{millify_tao(pool.price.tao)} τ/{symbol}" + if not verbose + else f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) # f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv swap_value, # Swap(α) -> τ "YES" @@ -2165,11 +2196,17 @@ def create_live_table( total_tao_value = Balance(0) total_swapped_tao_value = Balance(0) - def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): + def format_cell( + value, previous_value, unit="", unit_first=False, precision=4, millify=False + ): if previous_value is not None: change = value - previous_value if abs(change) > 10 ** (-precision): - formatted_change = f"{change:.{precision}f}" if not millify else f"{millify_tao(change)}" + formatted_change = ( + f"{change:.{precision}f}" + if not millify + else f"{millify_tao(change)}" + ) change_text = ( f" [pale_green3](+{formatted_change})[/pale_green3]" if change > 0 @@ -2179,19 +2216,23 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m change_text = "" else: change_text = "" - formatted_value = f"{value:,.{precision}f}" if not millify else f"{millify_tao(value)}" + formatted_value = ( + f"{value:,.{precision}f}" if not millify else f"{millify_tao(value)}" + ) return ( f"{formatted_value} {unit}{change_text}" if not unit_first else f"{unit} {formatted_value}{change_text}" ) - + # Sort subnets by value root_stakes = [s for s in substakes if s.netuid == 0] other_stakes = sorted( [s for s in substakes if s.netuid != 0], - key=lambda x: dynamic_info[x.netuid].alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)).tao, - reverse=True + key=lambda x: dynamic_info[x.netuid] + .alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid)) + .tao, + reverse=True, ) sorted_substakes = root_stakes + other_stakes @@ -2296,7 +2337,10 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m unit_first=unit_first, precision=4, ) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + + f" {SUBNETS.get(netuid, '~')}" + ) rows.append( [ @@ -2464,8 +2508,16 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m input() balance = await subtensor.get_balance(coldkey_address) - total_tao_value = f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}" if not verbose else all_hotkeys_total_tao_value - total_tao_ownership = f"τ {millify_tao(all_hotkeys_total_global_tao.tao)}" if not verbose else all_hotkeys_total_global_tao + total_tao_value = ( + f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}" + if not verbose + else all_hotkeys_total_tao_value + ) + total_tao_ownership = ( + f"τ {millify_tao(all_hotkeys_total_global_tao.tao)}" + if not verbose + else all_hotkeys_total_global_tao + ) console.print("\n\n") console.print( @@ -2575,19 +2627,23 @@ async def move_stake( # Get the wallet stake balances. origin_stake_balance = Balance.from_rao(0) destination_stake_balance = Balance.from_rao(0) - + chain_head = await subtensor.substrate.get_chain_head() stake_info_dict = await subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], block_hash=chain_head, ) - + for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: - if (stake_info.hotkey_ss58 == origin_hotkey_ss58 and - stake_info.netuid == origin_netuid): + if ( + stake_info.hotkey_ss58 == origin_hotkey_ss58 + and stake_info.netuid == origin_netuid + ): origin_stake_balance = stake_info.stake - elif (stake_info.hotkey_ss58 == destination_hotkey and - stake_info.netuid == destination_netuid): + elif ( + stake_info.hotkey_ss58 == destination_hotkey + and stake_info.netuid == destination_netuid + ): destination_stake_balance = stake_info.stake # Set appropriate units @@ -2786,17 +2842,25 @@ async def move_stake( new_stake_info_dict = await subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], ) - + new_origin_stake_balance = Balance.from_rao(0) new_destination_stake_balance = Balance.from_rao(0) - + for stake_info in new_stake_info_dict[wallet.coldkeypub.ss58_address]: - if (stake_info.hotkey_ss58 == origin_hotkey_ss58 and - stake_info.netuid == origin_netuid): - new_origin_stake_balance = stake_info.stake.set_unit(origin_netuid) - elif (stake_info.hotkey_ss58 == destination_hotkey and - stake_info.netuid == destination_netuid): - new_destination_stake_balance = stake_info.stake.set_unit(destination_netuid) + if ( + stake_info.hotkey_ss58 == origin_hotkey_ss58 + and stake_info.netuid == origin_netuid + ): + new_origin_stake_balance = stake_info.stake.set_unit( + origin_netuid + ) + elif ( + stake_info.hotkey_ss58 == destination_hotkey + and stake_info.netuid == destination_netuid + ): + new_destination_stake_balance = stake_info.stake.set_unit( + destination_netuid + ) console.print( f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " @@ -2816,31 +2880,30 @@ async def fetch_coldkey_stake(subtensor: "SubtensorInterface", wallet: Wallet): return sub_stakes -# TODO: Use this in all subnet commands. +# TODO: Use this in all subnet commands. async def get_stake_info_for_coldkey_and_hotkey( subtensor: "SubtensorInterface", coldkey_ss58: str, hotkey_ss58: Optional[str] = None, netuid: Optional[int] = None, - block_hash: Optional[str] = None + block_hash: Optional[str] = None, ) -> dict[tuple[str, int], Balance]: """Helper function to get stake info for a coldkey and optionally filter by hotkey and netuid. - + Args: subtensor: SubtensorInterface instance coldkey_ss58: Coldkey SS58 address hotkey_ss58: Optional hotkey SS58 address to filter by netuid: Optional netuid to filter by block_hash: Optional block hash to query at - + Returns: Dictionary mapping (hotkey, netuid) tuple to stake balance """ stake_info_dict = await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[coldkey_ss58], - block_hash=block_hash + coldkey_ss58_list=[coldkey_ss58], block_hash=block_hash ) - + stakes = {} for stake_info in stake_info_dict[coldkey_ss58]: if hotkey_ss58 and stake_info.hotkey_ss58 != hotkey_ss58: @@ -2848,5 +2911,5 @@ async def get_stake_info_for_coldkey_and_hotkey( if netuid is not None and stake_info.netuid != netuid: continue stakes[(stake_info.hotkey_ss58, stake_info.netuid)] = stake_info.stake - + return stakes diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index c06e2eb5..1e7d49a5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -186,28 +186,35 @@ async def fetch_subnet_data(): other_subnets = sorted( [s for s in subnets if s.netuid != 0], key=lambda x: (x.alpha_in.tao + x.alpha_out.tao) * x.price.tao, - reverse=True + reverse=True, ) sorted_subnets = [root_subnet] + other_subnets return sorted_subnets, subnet_tao, block_number - - def calculate_emission_stats(subnet_tao: dict, block_number: int) -> tuple[Balance, str]: + + def calculate_emission_stats( + subnet_tao: dict, block_number: int + ) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation total_tao_emitted = sum( - subnet_tao.get(n, Balance(0)) - for n in subnet_tao.keys() - if n != 0 + subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0 ) emission_percentage = (total_tao_emitted.tao / block_number) * 100 percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" - formatted_percentage = f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" + formatted_percentage = ( + f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" + ) if not verbose: percentage_string = f"τ {millify_tao(total_tao_emitted.tao)}/{millify_tao(block_number)} ({formatted_percentage})" else: percentage_string = f"τ {total_tao_emitted.tao:,.1f}/{block_number} ({formatted_percentage})" return total_tao_emitted, percentage_string - def define_table(total_emissions: float, total_rate: float, total_netuids: int, tao_emission_percentage: str): + def define_table( + total_emissions: float, + total_rate: float, + total_netuids: int, + tao_emission_percentage: str, + ): table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", @@ -221,7 +228,12 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, pad_edge=True, ) - table.add_column("[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids)) + table.add_column( + "[bold white]Netuid", + style="grey89", + justify="center", + footer=str(total_netuids), + ) table.add_column("[bold white]Name", style="cyan", justify="left") table.add_column( f"[bold white]Price \n({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", @@ -267,7 +279,7 @@ def define_table(total_emissions: float, total_rate: float, total_netuids: int, # Non-live mode def create_table(subnets, subnet_tao, block_number): - rows = [] + rows = [] _, percentage_string = calculate_emission_stats(subnet_tao, block_number) for subnet in subnets: @@ -278,18 +290,40 @@ def create_table(subnets, subnet_tao, block_number): emission_tao = 0.0 else: emission_tao = subnet.emission.tao - - alpha_in_value = f"{millify_tao(subnet.alpha_in.tao)}" if not verbose else f"{subnet.alpha_in.tao:,.4f}" - alpha_out_value = f"{millify_tao(subnet.alpha_out.tao)}" if not verbose else f"{subnet.alpha_out.tao:,.4f}" - price_value = f"{millify_tao(subnet.price.tao)}" if not verbose else f"{subnet.price.tao:,.4f}" - + + alpha_in_value = ( + f"{millify_tao(subnet.alpha_in.tao)}" + if not verbose + else f"{subnet.alpha_in.tao:,.4f}" + ) + alpha_out_value = ( + f"{millify_tao(subnet.alpha_out.tao)}" + if not verbose + else f"{subnet.alpha_out.tao:,.4f}" + ) + price_value = ( + f"{millify_tao(subnet.price.tao)}" + if not verbose + else f"{subnet.price.tao:,.4f}" + ) + # Market Cap market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao - market_cap_value = f"{millify_tao(market_cap)}" if not verbose else f"{market_cap:,.4f}" + market_cap_value = ( + f"{millify_tao(market_cap)}" if not verbose else f"{market_cap:,.4f}" + ) # Liquidity - tao_in_cell = f"τ {millify_tao(subnet.tao_in.tao)}" if not verbose else f"τ {subnet.tao_in.tao:,.4f}" - alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_in_value}" + tao_in_cell = ( + f"τ {millify_tao(subnet.tao_in.tao)}" + if not verbose + else f"τ {subnet.tao_in.tao:,.4f}" + ) + alpha_in_cell = ( + f"{alpha_in_value} {symbol}" + if netuid != 0 + else f"{symbol} {alpha_in_value}" + ) # Supply supply = subnet.alpha_in.tao + subnet.alpha_out.tao @@ -297,26 +331,33 @@ def create_table(subnets, subnet_tao, block_number): # Prepare cells netuid_cell = str(netuid) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + + f" {SUBNETS.get(netuid, '~')}" + ) emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{price_value} τ/{symbol}" liquidity_cell = f"{tao_in_cell}, {alpha_in_cell}" - alpha_out_cell = f"{alpha_out_value} {symbol}" if netuid != 0 else f"{symbol} {alpha_out_value}" + alpha_out_cell = ( + f"{alpha_out_value} {symbol}" + if netuid != 0 + else f"{symbol} {alpha_out_value}" + ) market_cap_cell = f"τ {market_cap_value}" supply_cell = f"{supply_value} {symbol} [#806DAF]/21M" tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" rows.append( ( - netuid_cell, # Netuid - subnet_name_cell, # Name - price_cell, # Rate τ_in/α_in + netuid_cell, # Netuid + subnet_name_cell, # Name + price_cell, # Rate τ_in/α_in market_cap_cell, # Market Cap - emission_cell, # Emission (τ) - liquidity_cell, # Liquidity (t_in, a_in) - alpha_out_cell, # Stake α_out - supply_cell, # Supply - tempo_cell, # Tempo k/n + emission_cell, # Emission (τ) + liquidity_cell, # Liquidity (t_in, a_in) + alpha_out_cell, # Stake α_out + supply_cell, # Supply + tempo_cell, # Tempo k/n ) ) @@ -327,7 +368,9 @@ def create_table(subnets, subnet_tao, block_number): float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) total_netuids = len(subnets) - table = define_table(total_emissions, total_rate, total_netuids, percentage_string) + table = define_table( + total_emissions, total_rate, total_netuids, percentage_string + ) for row in rows: table.add_row(*row) @@ -335,11 +378,17 @@ def create_table(subnets, subnet_tao, block_number): # Live mode def create_table_live(subnets, previous_data, subnet_tao, block_number): - def format_cell(value, previous_value, unit="", unit_first=False, precision=4, millify=False): + def format_cell( + value, previous_value, unit="", unit_first=False, precision=4, millify=False + ): if previous_value is not None: change = value - previous_value if abs(change) > 10 ** (-precision): - formatted_change = f"{change:.{precision}f}" if not millify else f"{millify_tao(change)}" + formatted_change = ( + f"{change:.{precision}f}" + if not millify + else f"{millify_tao(change)}" + ) change_text = ( f" [pale_green3](+{formatted_change})[/pale_green3]" if change > 0 @@ -349,39 +398,85 @@ def format_cell(value, previous_value, unit="", unit_first=False, precision=4, m change_text = "" else: change_text = "" - formatted_value = f"{value:,.{precision}f}" if not millify else millify_tao(value) - return f"{formatted_value} {unit}{change_text}" if not unit_first else f"{unit} {formatted_value}{change_text}" + formatted_value = ( + f"{value:,.{precision}f}" if not millify else millify_tao(value) + ) + return ( + f"{formatted_value} {unit}{change_text}" + if not unit_first + else f"{unit} {formatted_value}{change_text}" + ) - def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, precision=4, millify=False, netuid=None): + def format_liquidity_cell( + tao_val, + alpha_val, + prev_tao, + prev_alpha, + symbol, + precision=4, + millify=False, + netuid=None, + ): """Format liquidity cell with combined changes""" - tao_str = f"τ {millify_tao(tao_val)}" if millify else f"τ {tao_val:,.{precision}f}" + tao_str = ( + f"τ {millify_tao(tao_val)}" + if millify + else f"τ {tao_val:,.{precision}f}" + ) _alpha_str = f"{millify_tao(alpha_val) if millify else f'{alpha_val:,.{precision}f}'}" - alpha_str = f"{_alpha_str} {symbol}" if netuid != 0 else f"{symbol} {_alpha_str}" - + alpha_str = ( + f"{_alpha_str} {symbol}" if netuid != 0 else f"{symbol} {_alpha_str}" + ) + # Show delta if prev_tao is not None and prev_alpha is not None: tao_change = tao_val - prev_tao alpha_change = alpha_val - prev_alpha - + # Show changes if either value changed - if abs(tao_change) > 10**(-precision) or abs(alpha_change) > 10**(-precision): - + if abs(tao_change) > 10 ** (-precision) or abs(alpha_change) > 10 ** ( + -precision + ): if millify: - tao_change_str = f"+{millify_tao(tao_change)}" if tao_change > 0 else f"{millify_tao(tao_change)}" - alpha_change_str = f"+{millify_tao(alpha_change)}" if alpha_change > 0 else f"{millify_tao(alpha_change)}" + tao_change_str = ( + f"+{millify_tao(tao_change)}" + if tao_change > 0 + else f"{millify_tao(tao_change)}" + ) + alpha_change_str = ( + f"+{millify_tao(alpha_change)}" + if alpha_change > 0 + else f"{millify_tao(alpha_change)}" + ) else: - tao_change_str = f"+{tao_change:.{precision}f}" if tao_change > 0 else f"{tao_change:.{precision}f}" - alpha_change_str = f"+{alpha_change:.{precision}f}" if alpha_change > 0 else f"{alpha_change:.{precision}f}" - - changes_str = f" [pale_green3]({tao_change_str}[/pale_green3]" if tao_change > 0 else \ - f" [hot_pink3]({tao_change_str}[/hot_pink3]" if tao_change < 0 else \ - f" [white]({tao_change_str}[/white]" - changes_str += f"[pale_green3],{alpha_change_str})[/pale_green3]" if alpha_change > 0 else \ - f"[hot_pink3],{alpha_change_str})[/hot_pink3]" if alpha_change < 0 else \ - f"[white],{alpha_change_str})[/white]" + tao_change_str = ( + f"+{tao_change:.{precision}f}" + if tao_change > 0 + else f"{tao_change:.{precision}f}" + ) + alpha_change_str = ( + f"+{alpha_change:.{precision}f}" + if alpha_change > 0 + else f"{alpha_change:.{precision}f}" + ) + + changes_str = ( + f" [pale_green3]({tao_change_str}[/pale_green3]" + if tao_change > 0 + else f" [hot_pink3]({tao_change_str}[/hot_pink3]" + if tao_change < 0 + else f" [white]({tao_change_str}[/white]" + ) + changes_str += ( + f"[pale_green3],{alpha_change_str})[/pale_green3]" + if alpha_change > 0 + else f"[hot_pink3],{alpha_change_str})[/hot_pink3]" + if alpha_change < 0 + else f"[white],{alpha_change_str})[/white]" + ) return f"{tao_str}, {alpha_str}{changes_str}" - + return f"{tao_str}, {alpha_str}" rows = [] @@ -420,12 +515,23 @@ def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, prec unit_first = False netuid_cell = str(netuid) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03A4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + + f" {SUBNETS.get(netuid, '~')}" + ) emission_cell = format_cell( - emission_tao, prev.get("emission_tao"), unit="τ", unit_first=True, precision=4 + emission_tao, + prev.get("emission_tao"), + unit="τ", + unit_first=True, + precision=4, ) price_cell = format_cell( - subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4, millify=True if not verbose else False + subnet.price.tao, + prev.get("price"), + unit=f"τ/{symbol}", + precision=4, + millify=True if not verbose else False, ) alpha_out_cell = format_cell( @@ -446,7 +552,7 @@ def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, prec millify=not verbose, netuid=netuid, ) - + market_cap_cell = format_cell( market_cap, prev.get("market_cap"), @@ -492,30 +598,38 @@ def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, prec rows.append( ( - netuid_cell, # Netuid - subnet_name_cell, # Name - price_cell, # Rate τ_in/α_in + netuid_cell, # Netuid + subnet_name_cell, # Name + price_cell, # Rate τ_in/α_in market_cap_cell, # Market Cap - emission_cell, # Emission (τ) - liquidity_cell, # Liquidity (t_in, a_in) - alpha_out_cell, # Stake α_out - supply_cell, # Supply - tempo_cell, # Tempo k/n + emission_cell, # Emission (τ) + liquidity_cell, # Liquidity (t_in, a_in) + alpha_out_cell, # Stake α_out + supply_cell, # Supply + tempo_cell, # Tempo k/n ) ) - + # Calculate totals total_netuids = len(subnets) _total_emissions = sum( float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) - total_emissions = f"{millify_tao(_total_emissions)}" if not verbose else f"{_total_emissions:,.2f}" + total_emissions = ( + f"{millify_tao(_total_emissions)}" + if not verbose + else f"{_total_emissions:,.2f}" + ) total_rate = sum( float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 ) - total_rate = f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" - table = define_table(total_emissions, total_rate, total_netuids, percentage_string) + total_rate = ( + f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" + ) + table = define_table( + total_emissions, total_rate, total_netuids, percentage_string + ) for row in rows: table.add_row(*row) @@ -546,7 +660,11 @@ def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, prec # Update block numbers previous_block = current_block current_block = block_number - new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) + new_blocks = ( + "N/A" + if previous_block is None + else str(current_block - previous_block) + ) table, current_data = create_table_live( subnets, previous_data, subnet_tao, block_number @@ -618,13 +736,13 @@ def format_liquidity_cell(tao_val, alpha_val, prev_tao, prev_alpha, symbol, prec "[bold tan]Alpha Pool (α_in)[/bold tan]", "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].", ), - ( + ( "[bold tan]STAKE (α_out)[/bold tan]", "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", ), ( "[bold tan]RATE (τ_in/α_in)[/bold tan]", - 'Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].', + "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", ), ( "[bold tan]Tempo (k/n)[/bold tan]", @@ -747,20 +865,36 @@ async def show_root(): ) # Get identity for this validator - coldkey_identity = identities.get(root_state.coldkeys[idx], {}).get("name", "") + coldkey_identity = identities.get(root_state.coldkeys[idx], {}).get( + "name", "" + ) hotkey_identity = old_identities.get(root_state.hotkeys[idx]) - validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") + validator_identity = ( + coldkey_identity + if coldkey_identity + else (hotkey_identity.display if hotkey_identity else "") + ) sorted_rows.append( ( - str((pos + 1)), # Position - f"τ {millify_tao(root_state.total_stake[idx].tao)}" if not verbose else f"{root_state.total_stake[idx]}", # Total Stake - f"τ {root_state.alpha_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(root_state.alpha_stake[idx])}", # Alpha Stake - f"τ {root_state.tao_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(root_state.tao_stake[idx])}", # Tao Stake - f"{total_emission_per_block}", # Emission - f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", # Hotkey - f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", # Coldkey - validator_identity, # Identity + str((pos + 1)), # Position + f"τ {millify_tao(root_state.total_stake[idx].tao)}" + if not verbose + else f"{root_state.total_stake[idx]}", # Total Stake + f"τ {root_state.alpha_stake[idx].tao:.4f}" + if verbose + else f"τ {millify_tao(root_state.alpha_stake[idx])}", # Alpha Stake + f"τ {root_state.tao_stake[idx].tao:.4f}" + if verbose + else f"τ {millify_tao(root_state.tao_stake[idx])}", # Tao Stake + f"{total_emission_per_block}", # Emission + f"{root_state.hotkeys[idx][:6]}" + if not verbose + else f"{root_state.hotkeys[idx]}", # Hotkey + f"{root_state.coldkeys[idx][:6]}" + if not verbose + else f"{root_state.coldkeys[idx]}", # Coldkey + validator_identity, # Identity ) ) sorted_hks_delegation.append(root_state.hotkeys[idx]) @@ -778,10 +912,26 @@ async def show_root(): console.print("\n") if not delegate_selection: - tao_pool = f"{millify_tao(root_info.tao_in.tao)}" if not verbose else f"{root_info.tao_in.tao:,.4f}" - alpha_pool = f"{millify_tao(root_info.alpha_in.tao)}" if not verbose else f"{root_info.alpha_in.tao:,.4f}" - stake = f"{millify_tao(root_info.alpha_out.tao)}" if not verbose else f"{root_info.alpha_out.tao:,.5f}" - rate = f"{millify_tao(root_info.price.tao)}" if not verbose else f"{root_info.price.tao:,.4f}" + tao_pool = ( + f"{millify_tao(root_info.tao_in.tao)}" + if not verbose + else f"{root_info.tao_in.tao:,.4f}" + ) + alpha_pool = ( + f"{millify_tao(root_info.alpha_in.tao)}" + if not verbose + else f"{root_info.alpha_in.tao:,.4f}" + ) + stake = ( + f"{millify_tao(root_info.alpha_out.tao)}" + if not verbose + else f"{root_info.alpha_out.tao:,.5f}" + ) + rate = ( + f"{millify_tao(root_info.price.tao)}" + if not verbose + else f"{root_info.price.tao:,.4f}" + ) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{rate} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" @@ -808,29 +958,38 @@ async def show_root(): while True: selection = Prompt.ask( "\nEnter the position of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", - default="" + default="", ) - + if selection == "": return None - + try: idx = int(selection) if 1 <= idx <= max_rows: - selected_hotkey = sorted_hks_delegation[idx - 1] - row_data = sorted_rows[idx - 1] - identity = "" if row_data[5] == "~" else row_data[5] - identity_str = f" ({identity})" if identity else "" - console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}") - - return selected_hotkey + selected_hotkey = sorted_hks_delegation[idx - 1] + row_data = sorted_rows[idx - 1] + identity = "" if row_data[5] == "~" else row_data[5] + identity_str = f" ({identity})" if identity else "" + console.print( + f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" + ) + + return selected_hotkey else: - console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + console.print( + f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]" + ) except ValueError: console.print("[red]Please enter a valid number[/red]") async def show_subnet(netuid_: int): - subnet_info, hex_bytes_result, identities, old_identities = await asyncio.gather( + ( + subnet_info, + hex_bytes_result, + identities, + old_identities, + ) = await asyncio.gather( subtensor.get_subnet_dynamic_info(netuid_), subtensor.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", @@ -840,9 +999,14 @@ async def show_subnet(netuid_: int): subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - owner_ss58 = subnet_info.owner if subnet_info else "" - owner_identity = identities.get(owner_ss58, {}).get("name", old_identities.get(owner_ss58).display if old_identities.get(owner_ss58) else "") - + owner_ss58 = subnet_info.owner if subnet_info else "" + owner_identity = identities.get(owner_ss58, {}).get( + "name", + old_identities.get(owner_ss58).display + if old_identities.get(owner_ss58) + else "", + ) + if (bytes_result := hex_bytes_result) is None: err_console.print(f"Subnet {netuid_} does not exist") return @@ -874,7 +1038,7 @@ async def show_subnet(netuid_: int): show_lines=False, pad_edge=True, ) - + # Add index for selection if selecting delegates if delegate_selection: table.add_column("#", style="cyan", justify="right") @@ -898,28 +1062,49 @@ async def show_subnet(netuid_: int): ) relative_emissions_sum += hotkey_block_emission tao_sum += subnet_state.total_stake[idx] - + # Get identity for this uid - coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get("name", "") + coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( + "name", "" + ) hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) - uid_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "~") - - if subnet_state.coldkeys[idx] == subnet_info.owner or subnet_state.hotkeys[idx] in owner_hotkeys: - uid_identity = f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" + uid_identity = ( + coldkey_identity + if coldkey_identity + else (hotkey_identity.display if hotkey_identity else "~") + ) + + if ( + subnet_state.coldkeys[idx] == subnet_info.owner + or subnet_state.hotkeys[idx] in owner_hotkeys + ): + uid_identity = ( + f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" + ) rows.append( ( str(idx), # UID - f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake - f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake - f"τ {subnet_state.tao_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(subnet_state.tao_stake[idx])}", # Tao Stake + f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" + if verbose + else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake + f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" + if verbose + else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake + f"τ {subnet_state.tao_stake[idx].tao:.4f}" + if verbose + else f"τ {millify_tao(subnet_state.tao_stake[idx])}", # Tao Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends str(subnet_state.incentives[idx]), # Incentive # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions - f"{subnet_state.hotkeys[idx][:6]}" if not verbose else f"{subnet_state.hotkeys[idx]}", # Hotkey - f"{subnet_state.coldkeys[idx][:6]}" if not verbose else f"{subnet_state.coldkeys[idx]}", # Coldkey + f"{subnet_state.hotkeys[idx][:6]}" + if not verbose + else f"{subnet_state.hotkeys[idx]}", # Hotkey + f"{subnet_state.coldkeys[idx][:6]}" + if not verbose + else f"{subnet_state.coldkeys[idx]}", # Coldkey uid_identity, # Identity ) ) @@ -928,7 +1113,7 @@ async def show_subnet(netuid_: int): sorted_rows = sorted( rows, key=lambda x: float(str(x[2]).split()[0].replace(",", "")), - reverse=True + reverse=True, ) # Add columns to the table @@ -938,7 +1123,9 @@ async def show_subnet(netuid_: int): style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", - footer=f"{tao_sum.set_unit(subnet_info.netuid)}" if verbose else f"{millify_tao(tao_sum.tao)} {subnet_info.symbol}", + footer=f"{tao_sum.set_unit(subnet_info.netuid)}" + if verbose + else f"{millify_tao(tao_sum.tao)} {subnet_info.symbol}", ) # ------- Temporary columns for testing ------- table.add_column( @@ -1011,10 +1198,18 @@ async def show_subnet(netuid_: int): console.print("\n") if not delegate_selection: - subnet_name = SUBNETS.get(netuid_, '') + subnet_name = SUBNETS.get(netuid_, "") subnet_name_display = f": {subnet_name}" if subnet_name else "" - tao_pool = f"{millify_tao(subnet_info.tao_in.tao)}" if not verbose else f"{subnet_info.tao_in.tao:,.4f}" - alpha_pool = f"{millify_tao(subnet_info.alpha_in.tao)}" if not verbose else f"{subnet_info.alpha_in.tao:,.4f}" + tao_pool = ( + f"{millify_tao(subnet_info.tao_in.tao)}" + if not verbose + else f"{subnet_info.tao_in.tao:,.4f}" + ) + alpha_pool = ( + f"{millify_tao(subnet_info.alpha_in.tao)}" + if not verbose + else f"{subnet_info.alpha_in.tao:,.4f}" + ) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" @@ -1047,24 +1242,28 @@ async def show_subnet(netuid_: int): while True: selection = Prompt.ask( "\nEnter the number of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", - default="" + default="", ) - + if selection == "": return None - + try: idx = int(selection) if 1 <= idx <= max_rows: - uid = int(sorted_rows[idx-1][0]) + uid = int(sorted_rows[idx - 1][0]) hotkey = subnet_state.hotkeys[uid] - row_data = sorted_rows[idx-1] + row_data = sorted_rows[idx - 1] identity = "" if row_data[7] == "~" else row_data[7] identity_str = f" ({identity})" if identity else "" - console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}") + console.print( + f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}" + ) return hotkey else: - console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + console.print( + f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]" + ) except ValueError: console.print("[red]Please enter a valid number[/red]") @@ -1077,6 +1276,7 @@ async def show_subnet(netuid_: int): result = await show_subnet(netuid) return result + async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( From ce403e4da540140d8db44e0f5c7949e30bf56239 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 3 Jan 2025 18:54:39 -0800 Subject: [PATCH 180/332] Bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index f5baec6f..e0b9ba83 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc1" +__version__ = "8.2.0rc2" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a200978a..89e0e3db 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -61,7 +61,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc1" +__version__ = "8.2.0rc2" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 8074a7675c8a4b15a0d3654cf06e8e33486efbf9 Mon Sep 17 00:00:00 2001 From: unconst Date: Sat, 4 Jan 2025 14:58:53 -0500 Subject: [PATCH 181/332] fix back slash --- bittensor_cli/src/commands/stake/stake.py | 7 +++---- bittensor_cli/src/commands/subnets.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index a7b81369..938cbe57 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2146,8 +2146,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else f"{substake_.stake.tao:,.4f}" ) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - + f" {SUBNETS.get(netuid, '~')}" + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {SUBNETS.get(netuid, '~')}" ) rows.append( @@ -2338,8 +2337,8 @@ def format_cell( precision=4, ) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - + f" {SUBNETS.get(netuid, '~')}" + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" ) rows.append( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 1e7d49a5..6401a664 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -332,8 +332,8 @@ def create_table(subnets, subnet_tao, block_number): # Prepare cells netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - + f" {SUBNETS.get(netuid, '~')}" + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" ) emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{price_value} τ/{symbol}" @@ -516,8 +516,8 @@ def format_liquidity_cell( netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else '\u03a4'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - + f" {SUBNETS.get(netuid, '~')}" + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {SUBNETS.get(netuid, '~')}" ) emission_cell = format_cell( emission_tao, From 300fcf629efc0796dce8fd813c04b522932151cf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sat, 4 Jan 2025 14:30:24 -0800 Subject: [PATCH 182/332] Fixed sorting breaking + pool column changes --- bittensor_cli/src/commands/subnets.py | 64 ++++++++++++++------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 6401a664..8c74b6b5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -253,7 +253,7 @@ def define_table( footer=f"τ {total_emissions}", ) table.add_column( - f"[bold white]Liquidity \n({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", + f"[bold white]P ({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", footer=f"{tao_emission_percentage}", @@ -315,16 +315,17 @@ def create_table(subnets, subnet_tao, block_number): # Liquidity tao_in_cell = ( - f"τ {millify_tao(subnet.tao_in.tao)}" - if not verbose - else f"τ {subnet.tao_in.tao:,.4f}" - ) - alpha_in_cell = ( - f"{alpha_in_value} {symbol}" + ( + f"τ {millify_tao(subnet.tao_in.tao)}" + if not verbose + else f"τ {subnet.tao_in.tao:,.4f}" + ) if netuid != 0 - else f"{symbol} {alpha_in_value}" + else "-" ) + alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else "-" + # Supply supply = subnet.alpha_in.tao + subnet.alpha_out.tao supply_value = f"{millify_tao(supply)}" if not verbose else f"{supply:,.4f}" @@ -542,15 +543,19 @@ def format_liquidity_cell( precision=5, millify=True if not verbose else False, ) - liquidity_cell = format_liquidity_cell( - subnet.tao_in.tao, - subnet.alpha_in.tao, - prev.get("tao_in"), - prev.get("alpha_in"), - symbol, - precision=4, - millify=not verbose, - netuid=netuid, + liquidity_cell = ( + format_liquidity_cell( + subnet.tao_in.tao, + subnet.alpha_in.tao, + prev.get("tao_in"), + prev.get("alpha_in"), + symbol, + precision=4, + millify=not verbose, + netuid=netuid, + ) + if netuid != 0 + else "-, -" ) market_cap_cell = format_cell( @@ -969,7 +974,7 @@ async def show_root(): if 1 <= idx <= max_rows: selected_hotkey = sorted_hks_delegation[idx - 1] row_data = sorted_rows[idx - 1] - identity = "" if row_data[5] == "~" else row_data[5] + identity = "" if row_data[7] == "~" else row_data[7] identity_str = f" ({identity})" if identity else "" console.print( f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" @@ -1054,7 +1059,13 @@ async def show_subnet(netuid_: int): relative_emissions_sum = 0 owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner) - for idx, hk in enumerate(subnet_state.hotkeys): + sorted_indices = sorted( + range(len(subnet_state.hotkeys)), + key=lambda i: subnet_state.total_stake[i].tao, + reverse=True, + ) + + for idx in sorted_indices: hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum if emission_sum != 0 @@ -1109,13 +1120,6 @@ async def show_subnet(netuid_: int): ) ) - # Sort rows by stake - sorted_rows = sorted( - rows, - key=lambda x: float(str(x[2]).split()[0].replace(",", "")), - reverse=True, - ) - # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( @@ -1183,7 +1187,7 @@ async def show_subnet(netuid_: int): no_wrap=True, justify="left", ) - for pos, row in enumerate(sorted_rows, 1): + for pos, row in enumerate(rows, 1): table_row = [] if delegate_selection: table_row.append(str(pos)) @@ -1251,10 +1255,10 @@ async def show_subnet(netuid_: int): try: idx = int(selection) if 1 <= idx <= max_rows: - uid = int(sorted_rows[idx - 1][0]) + uid = int(rows[idx - 1][0]) hotkey = subnet_state.hotkeys[uid] - row_data = sorted_rows[idx - 1] - identity = "" if row_data[7] == "~" else row_data[7] + row_data = rows[idx - 1] + identity = "" if row_data[9] == "~" else row_data[9] identity_str = f" ({identity})" if identity else "" console.print( f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}" From c34935633bb2ff0a3f524d38a4ab00a0044c5bd9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 09:15:57 -0800 Subject: [PATCH 183/332] Tempo should be NA for root --- bittensor_cli/src/commands/subnets.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 8c74b6b5..30381fd9 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -346,7 +346,11 @@ def create_table(subnets, subnet_tao, block_number): ) market_cap_cell = f"τ {market_cap_value}" supply_cell = f"{supply_value} {symbol} [#806DAF]/21M" - tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" + + if netuid != 0: + tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" + else: + tempo_cell = "[red]N/A[/red]" rows.append( ( @@ -598,7 +602,9 @@ def format_liquidity_cell( else: block_change_text = "" tempo_cell = ( - f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" + (f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}") + if netuid != 0 + else "[red]N/A[/red]" ) rows.append( From a7c84f0474e52dafd76eb21efa99aa1ba4256e08 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 14:13:47 -0800 Subject: [PATCH 184/332] New dynamicinfo, st list updated --- bittensor_cli/src/bittensor/chain_data.py | 130 ++++++++++++++++++---- bittensor_cli/src/commands/stake/stake.py | 116 ++++++++++++------- 2 files changed, 182 insertions(+), 64 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 01b5c7f3..15728845 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -30,6 +30,7 @@ class ChainDataType(Enum): ScheduledColdkeySwapInfo = 13 SubnetInfo = 14 SubnetState = 15 + SubnetIdentity = 16 def from_scale_encoding_using_type_string( @@ -845,10 +846,37 @@ def fix_decoded_values(cls, decoded: dict) -> "SubnetInfoV2": ) +@dataclass +class SubnetIdentity: + """Dataclass for subnet identity information.""" + + subnet_name: str + github_repo: str + subnet_contact: str + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetIdentity"]: + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetIdentity) + if decoded is None: + return None + + return SubnetIdentity( + subnet_name=bytes(decoded["subnet_name"]).decode(), + github_repo=bytes(decoded["github_repo"]).decode(), + subnet_contact=bytes(decoded["subnet_contact"]).decode(), + ) + + @dataclass class DynamicInfo: - owner: str netuid: int + owner_hotkey: str + owner_coldkey: str + subnet_name: str + token_symbol: str tempo: int last_step: int blocks_since_last_step: int @@ -856,12 +884,16 @@ class DynamicInfo: alpha_in: Balance alpha_out: Balance tao_in: Balance - total_locked: Balance - owner_locked: Balance price: Balance k: float is_dynamic: bool - symbol: str + alpha_out_emission: Balance + alpha_in_emission: Balance + tao_in_emission: Balance + pending_alpha_emission: Balance + pending_root_emission: Balance + network_registered_at: int + subnet_identity: Optional[SubnetIdentity] @classmethod def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: @@ -884,37 +916,71 @@ def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo" @classmethod def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": + """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" netuid = int(decoded["netuid"]) - symbol = Balance.get_unit(netuid) + token_symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() + subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() + is_dynamic = ( + True if int(decoded["netuid"]) > 0 else False + ) # TODO: Patching this temporarily for netuid 0 + + owner_hotkey = ss58_encode(decoded["owner_hotkey"], SS58_FORMAT) + owner_coldkey = ss58_encode(decoded["owner_coldkey"], SS58_FORMAT) + emission = Balance.from_rao(decoded["emission"]).set_unit(0) - alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) + alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) - total_locked = Balance.from_rao(decoded["total_locked"]).set_unit(netuid) - owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit(netuid) + alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( + netuid + ) + alpha_in_emission = Balance.from_rao(decoded["alpha_in_emission"]).set_unit( + netuid + ) + tao_in_emission = Balance.from_rao(decoded["tao_in_emission"]).set_unit(0) + pending_alpha_emission = Balance.from_rao( + decoded["pending_alpha_emission"] + ).set_unit(netuid) + pending_root_emission = Balance.from_rao( + decoded["pending_root_emission"] + ).set_unit(0) price = ( - Balance.from_tao(1.0) if netuid == 0 + Balance.from_tao(1.0) + if netuid == 0 else Balance.from_tao(tao_in.tao / alpha_in.tao) if alpha_in.tao > 0 else Balance.from_tao(1) - ) # TODO: Patching this temporarily for netuid 0 - is_dynamic = True if int(decoded["netuid"]) > 0 else False # TODO: Patching this temporarily for netuid 0 - return DynamicInfo( - owner=ss58_encode(decoded["owner"], SS58_FORMAT), + ) # TODO: Patching this temporarily for netuid 0 + + subnet_identity = ( + SubnetIdentity.from_vec_u8(decoded["subnet_identity"]) + if decoded.get("subnet_identity") + else None + ) + + return cls( netuid=netuid, - tempo=decoded["tempo"], - last_step=decoded["last_step"], - blocks_since_last_step=decoded["blocks_since_last_step"], + owner_hotkey=owner_hotkey, + owner_coldkey=owner_coldkey, + subnet_name=subnet_name, + token_symbol=token_symbol, + tempo=int(decoded["tempo"]), + last_step=int(decoded["last_step"]), + blocks_since_last_step=int(decoded["blocks_since_last_step"]), emission=emission, - alpha_out=alpha_out, alpha_in=alpha_in, + alpha_out=alpha_out, tao_in=tao_in, - total_locked=total_locked, - owner_locked=owner_locked, - price=price, k=tao_in.rao * alpha_in.rao, is_dynamic=is_dynamic, - symbol=symbol, + price=price, + alpha_out_emission=alpha_out_emission, + alpha_in_emission=alpha_in_emission, + tao_in_emission=tao_in_emission, + pending_alpha_emission=pending_alpha_emission, + pending_root_emission=pending_root_emission, + network_registered_at=int(decoded["network_registered_at"]), + subnet_identity=subnet_identity, ) def tao_to_alpha(self, tao: Balance) -> Balance: @@ -1496,8 +1562,11 @@ def decode(result: list[int]) -> list[dict]: "DynamicInfo": { "type": "struct", "type_mapping": [ - ["owner", "AccountId"], ["netuid", "Compact"], + ["owner_hotkey", "AccountId"], + ["owner_coldkey", "AccountId"], + ["subnet_name", "Vec>"], + ["token_symbol", "Vec>"], ["tempo", "Compact"], ["last_step", "Compact"], ["blocks_since_last_step", "Compact"], @@ -1505,8 +1574,13 @@ def decode(result: list[int]) -> list[dict]: ["alpha_in", "Compact"], ["alpha_out", "Compact"], ["tao_in", "Compact"], - ["total_locked", "Compact"], - ["owner_locked", "Compact"], + ["alpha_out_emission", "Compact"], + ["alpha_in_emission", "Compact"], + ["tao_in_emission", "Compact"], + ["pending_alpha_emission", "Compact"], + ["pending_root_emission", "Compact"], + ["network_registered_at", "Compact"], + ["subnet_identity", "Option"], ], }, "SubstakeElements": { @@ -1550,5 +1624,13 @@ def decode(result: list[int]) -> list[dict]: ["liquid_alpha_enabled", "bool"], ], }, + "SubnetIdentity": { + "type": "struct", + "type_mapping": [ + ["subnet_name", "Vec"], + ["github_repo", "Vec"], + ["subnet_contact", "Vec"], + ], + }, } } diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 938cbe57..c3b3a619 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1293,6 +1293,7 @@ async def unstake_selection( console.print("\n", table, "\n") # Ask which netuids to unstake from for the selected hotkey. + unstake_all = False if netuid is not None: selected_netuids = [netuid] else: @@ -1304,6 +1305,7 @@ async def unstake_selection( if netuid_input.lower() == "all": selected_netuids = list(netuid_stakes.keys()) + unstake_all = True break else: try: @@ -1326,7 +1328,7 @@ async def unstake_selection( hotkeys_to_unstake_from.append( (selected_hotkey_name, selected_hotkey_ss58, netuid_) ) - return hotkeys_to_unstake_from + return hotkeys_to_unstake_from, unstake_all def ask_unstake_amount( @@ -1544,7 +1546,7 @@ async def _unstake_all( call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function=call_function, - call_params={}, + call_params={"hotkey": wallet.hotkey.ss58_address}, ) success, error_message = await subtensor.sign_and_send_extrinsic( call=call, @@ -1605,7 +1607,7 @@ async def unstake( all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} if interactive: - hotkeys_to_unstake_from = await unstake_selection( + hotkeys_to_unstake_from, unstake_all_from_hk = await unstake_selection( subtensor, wallet, all_sn_dynamic_info, @@ -1706,6 +1708,7 @@ async def unstake( ) # Iterate over hotkeys and netuids to collect unstake operations + unstake_all_hk_ss58 = None for hotkey in hotkeys_to_unstake_from: if skip_remaining_subnets: break @@ -1734,10 +1737,11 @@ async def unstake( continue # No stake to unstake # Determine the amount we are unstaking. - if initial_amount: - amount_to_unstake_as_balance = Balance.from_tao(initial_amount) - elif unstake_all: + if unstake_all_from_hk or unstake_all: amount_to_unstake_as_balance = current_stake_balance + unstake_all_hk_ss58 = staking_address_ss58 + elif initial_amount: + amount_to_unstake_as_balance = Balance.from_tao(initial_amount) else: amount_to_unstake_as_balance = ask_unstake_amount( current_stake_balance, @@ -1883,24 +1887,12 @@ async def unstake( return False with console.status("\n:satellite: Performing unstaking operations...") as status: - for op in unstake_operations: - netuid_i = op["netuid"] - staking_address_name = op["hotkey_name"] - staking_address_ss58 = op["hotkey_ss58"] - amount = op["amount_to_unstake"] - current_stake_balance = op["current_stake_balance"] - - status.update( - f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..." - ) - + if unstake_all_from_hk: call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="remove_stake", + call_function="unstake_all", call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_unstaked": amount.rao, + "hotkey": unstake_all_hk_ss58, }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( @@ -1924,24 +1916,71 @@ async def unstake( wallet.coldkeypub.ss58_address ) new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake_info = await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], - ) - new_stake = Balance.from_rao(0) - for stake_info in new_stake_info[wallet.coldkeypub.ss58_address]: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) + else: + for op in unstake_operations: + netuid_i = op["netuid"] + staking_address_name = op["hotkey_name"] + staking_address_ss58 = op["hotkey_ss58"] + amount = op["amount_to_unstake"] + current_stake_balance = op["current_stake_balance"] + + status.update( + f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..." + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + else: + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) + else: + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake_info = await subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + ) + new_stake = Balance.from_rao(0) + for stake_info in new_stake_info[ + wallet.coldkeypub.ss58_address + ]: + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): + new_stake = stake_info.stake.set_unit(netuid_i) + break + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) @@ -2074,7 +2113,6 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): netuid = substake_.netuid pool = dynamic_info[netuid] symbol = f"{Balance.get_unit(netuid)}\u200e" - # TODO: what is this price var for? price = ( "{:.4f}{}".format( @@ -2145,9 +2183,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): if not verbose else f"{substake_.stake.tao:,.4f}" ) - subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {SUBNETS.get(netuid, '~')}" - ) + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {SUBNETS.get(netuid, '~')}" rows.append( [ From d291b7789cd7c9f497a2b7f9f34136c0e436d788 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 14:19:44 -0800 Subject: [PATCH 185/332] Updated chain data --- bittensor_cli/src/bittensor/chain_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 15728845..898f7d99 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -876,7 +876,7 @@ class DynamicInfo: owner_hotkey: str owner_coldkey: str subnet_name: str - token_symbol: str + symbol: str tempo: int last_step: int blocks_since_last_step: int @@ -918,7 +918,7 @@ def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo" def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" netuid = int(decoded["netuid"]) - token_symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() + symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() is_dynamic = ( True if int(decoded["netuid"]) > 0 else False @@ -963,7 +963,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": owner_hotkey=owner_hotkey, owner_coldkey=owner_coldkey, subnet_name=subnet_name, - token_symbol=token_symbol, + symbol=symbol, tempo=int(decoded["tempo"]), last_step=int(decoded["last_step"]), blocks_since_last_step=int(decoded["blocks_since_last_step"]), From 129d6e17bd37d5e655d9df1dd75f6624ff843e63 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 14:57:01 -0800 Subject: [PATCH 186/332] Update live interval --- bittensor_cli/src/commands/subnets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 30381fd9..251e27f6 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -648,7 +648,7 @@ def format_liquidity_cell( # Live mode if live: - refresh_interval = 15 # seconds + refresh_interval = 10 # seconds progress = Progress( TextColumn("[progress.description]{task.description}"), @@ -996,12 +996,12 @@ async def show_root(): async def show_subnet(netuid_: int): ( - subnet_info, + _subnet_info, hex_bytes_result, identities, old_identities, ) = await asyncio.gather( - subtensor.get_subnet_dynamic_info(netuid_), + subtensor.get_all_subnet_dynamic_info(), subtensor.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", @@ -1010,7 +1010,8 @@ async def show_subnet(netuid_: int): subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - owner_ss58 = subnet_info.owner if subnet_info else "" + subnet_info = _subnet_info[netuid_] + owner_ss58 = subnet_info.owner_coldkey if subnet_info else "" owner_identity = identities.get(owner_ss58, {}).get( "name", old_identities.get(owner_ss58).display From e690cbf8fe4e7e2ba194264e36b120c0b3add22d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 16:18:54 -0800 Subject: [PATCH 187/332] s show working --- bittensor_cli/src/bittensor/chain_data.py | 2 +- bittensor_cli/src/commands/subnets.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 898f7d99..2bad1020 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1291,7 +1291,7 @@ class SubnetState: def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: if len(vec_u8) == 0: return None - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState, is_option=True) + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState) if decoded is None: return None return SubnetState.fix_decoded_values(decoded) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 251e27f6..628d2f99 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -1064,7 +1064,7 @@ async def show_subnet(netuid_: int): ) tao_sum = Balance(0) relative_emissions_sum = 0 - owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner) + owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) sorted_indices = sorted( range(len(subnet_state.hotkeys)), @@ -1093,7 +1093,7 @@ async def show_subnet(netuid_: int): ) if ( - subnet_state.coldkeys[idx] == subnet_info.owner + subnet_state.coldkeys[idx] == subnet_info.owner_coldkey or subnet_state.hotkeys[idx] in owner_hotkeys ): uid_identity = ( @@ -1114,7 +1114,7 @@ async def show_subnet(netuid_: int): else f"τ {millify_tao(subnet_state.tao_stake[idx])}", # Tao Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends - str(subnet_state.incentives[idx]), # Incentive + f"{subnet_state.incentives[idx]:.4f}", # Incentive # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions f"{subnet_state.hotkeys[idx][:6]}" @@ -1224,7 +1224,7 @@ async def show_subnet(netuid_: int): console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" From d22723e76e9514e87e12afd6f83585525f151967 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 16:40:02 -0800 Subject: [PATCH 188/332] Round off sums --- bittensor_cli/src/commands/subnets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 628d2f99..c1618b6f 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -366,11 +366,12 @@ def create_table(subnets, subnet_tao, block_number): ) ) - total_emissions = sum( - float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 + total_emissions = round( + sum(float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0), + 4, ) - total_rate = sum( - float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + total_rate = round( + sum(float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0), 4 ) total_netuids = len(subnets) table = define_table( From 7027eb4734697b515c1c23c357a154e2c50bb21f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 19:04:01 -0800 Subject: [PATCH 189/332] SN identities + stake fix --- bittensor_cli/cli.py | 17 +++- bittensor_cli/src/bittensor/utils.py | 100 ++++++++++++++++++++++ bittensor_cli/src/commands/stake/stake.py | 13 ++- bittensor_cli/src/commands/subnets.py | 43 ++++++++-- 4 files changed, 156 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 89e0e3db..1ddc5ce3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -47,6 +47,7 @@ get_effective_network, prompt_for_identity, validate_uri, + prompt_for_subnet_identity, ) from typing_extensions import Annotated from textwrap import dedent @@ -3606,6 +3607,15 @@ def subnets_create( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + subnet_name: Optional[str] = typer.Option( + None, "--subnet-name", help="Name of the subnet" + ), + github_repo: Optional[str] = typer.Option( + None, "--github-repo", help="GitHub repository URL" + ), + subnet_contact: Optional[str] = typer.Option( + None, "--subnet-contact", help="Contact email for subnet" + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3627,8 +3637,13 @@ def subnets_create( ], validate=WV.WALLET, ) + identity = prompt_for_subnet_identity( + subnet_name=subnet_name, + github_repo=github_repo, + subnet_contact=subnet_contact, + ) success = self._run_command( - subnets.create(wallet, self.initialize_chain(network), prompt) + subnets.create(wallet, self.initialize_chain(network), identity, prompt) ) if success and prompt: diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index c261ddf4..2a404a69 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable from urllib.parse import urlparse from functools import partial +import re from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT @@ -1112,3 +1113,102 @@ def prompt_for_identity( ) return identity_fields + + +def prompt_for_subnet_identity( + subnet_name: Optional[str], + github_repo: Optional[str], + subnet_contact: Optional[str], +): + """ + Prompts the user for required subnet identity fields with validation. + Returns a dictionary with the updated fields. + + Args: + subnet_name (Optional[str]): Name of the subnet + github_repo (Optional[str]): GitHub repository URL + subnet_contact (Optional[str]): Contact information for subnet (email) + + Returns: + dict: Dictionary containing the subnet identity fields + """ + identity_fields = {} + + fields = [ + ( + "subnet_name", + "[blue]Subnet name [dim](optional)[/blue]", + subnet_name, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Subnet name must be <= 64 raw bytes.", + ), + ( + "github_repo", + "[blue]GitHub repository URL [dim](optional)[/blue]", + github_repo, + lambda x: x and not is_valid_github_url(x), + "[red]Error:[/red] Please enter a valid GitHub repository URL (e.g., https://github.com/username/repo).", + ), + ( + "subnet_contact", + "[blue]Contact email [dim](optional)[/blue]", + subnet_contact, + lambda x: x and not is_valid_contact(x), + "[red]Error:[/red] Please enter a valid email address.", + ), + ] + + for key, prompt, value, rejection_func, rejection_msg in fields: + if value: + if rejection_func(value): + raise ValueError(rejection_msg) + identity_fields[key] = value + else: + identity_fields[key] = retry_prompt( + prompt, + rejection=rejection_func, + rejection_text=rejection_msg, + default=None, # Maybe we can add some defaults later + show_default=True, + ) + + return identity_fields + + +def is_valid_github_url(url: str) -> bool: + """ + Validates if the provided URL is a valid GitHub repository URL. + + Args: + url (str): URL to validate + + Returns: + bool: True if valid GitHub repo URL, False otherwise + """ + try: + parsed = urlparse(url) + if parsed.netloc != "github.com": + return False + + # Check path follows github.com/user/repo format + path_parts = [p for p in parsed.path.split("/") if p] + if len(path_parts) < 2: # Need at least username/repo + return False + + return True + except: + return False + + +def is_valid_contact(contact: str) -> bool: + """ + Validates if the provided contact is a valid email address. + + Args: + contact (str): Contact information to validate + + Returns: + bool: True if valid email, False otherwise + """ + email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + return bool(re.match(email_pattern, contact)) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index c3b3a619..ab476dff 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -929,18 +929,14 @@ async def stake_add( hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] starting_chain_head = await subtensor.substrate.get_chain_head() - all_dynamic_info, stake_info_dict = await asyncio.gather( - asyncio.gather( - *[ - subtensor.get_subnet_dynamic_info(x, starting_chain_head) - for x in netuids - ] - ), + _all_dynamic_info, stake_info_dict = await asyncio.gather( + subtensor.get_all_subnet_dynamic_info(), subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], block_hash=starting_chain_head, ), ) + all_dynamic_info = {di.netuid: di for di in _all_dynamic_info} initial_stake_balances = {} for hotkey_ss58 in [x[1] for x in hotkeys_to_stake_to]: initial_stake_balances[hotkey_ss58] = {} @@ -960,8 +956,9 @@ async def stake_add( ) return False for hotkey in hotkeys_to_stake_to: - for netuid, dynamic_info in zip(netuids, all_dynamic_info): + for netuid in netuids: # Check that the subnet exists. + dynamic_info = all_dynamic_info.get(netuid) if not dynamic_info: err_console.print(f"Subnet with netuid: {netuid} does not exist.") continue diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index c1618b6f..a8807592 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -50,6 +50,7 @@ async def register_subnetwork_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, + subnet_identity: dict, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -112,6 +113,22 @@ async def _find_event_attributes_in_extrinsic_receipt( ): return False + has_identity = any(subnet_identity.values()) + if has_identity: + identity_data = { + "subnet_name": subnet_identity["subnet_name"].encode(), + "github_repo": subnet_identity["github_repo"].encode(), + "subnet_contact": subnet_identity["subnet_contact"].encode(), + } + for field, value in identity_data.items(): + max_size = 64 # bytes + if len(value) > max_size: + err_console.print( + f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" + f"Value '{value.decode()}' is {len(value)} bytes." + ) + return False + try: wallet.unlock_coldkey() except KeyFileError: @@ -119,15 +136,21 @@ async def _find_event_attributes_in_extrinsic_receipt( return False with console.status(":satellite: Registering subnet...", spinner="earth"): + call_params = { + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + } + call_function = "register_network" + if has_identity: + call_params["identity"] = identity_data + call_function = "register_network_with_identity" + substrate = subtensor.substrate # create extrinsic call call = await substrate.compose_call( call_module="SubtensorModule", - call_function="register_network", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "mechid": 1, - }, + call_function=call_function, + call_params=call_params, ) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey @@ -1311,15 +1334,19 @@ async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: return None -async def create(wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool): +async def create( + wallet: Wallet, subtensor: "SubtensorInterface", subnet_identity: dict, prompt: bool +): """Register a subnetwork""" # Call register command. - success = await register_subnetwork_extrinsic(subtensor, wallet, prompt=prompt) + success = await register_subnetwork_extrinsic( + subtensor, wallet, subnet_identity, prompt=prompt + ) if success and prompt: # Prompt for user to set identity. do_set_identity = Confirm.ask( - "Would you like to set your [blue]identity?[/blue]" + "Would you like to set your own [blue]identity?[/blue]" ) if do_set_identity: From 27adae7a5f467acc07d8a3ff13b3b9d1cea3e9ab Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 6 Jan 2025 19:06:50 -0800 Subject: [PATCH 190/332] More args for ids in sn create --- bittensor_cli/cli.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1ddc5ce3..87c45828 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3608,13 +3608,17 @@ def subnets_create( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, subnet_name: Optional[str] = typer.Option( - None, "--subnet-name", help="Name of the subnet" + None, "--subnet-name", "--name", help="Name of the subnet" ), github_repo: Optional[str] = typer.Option( - None, "--github-repo", help="GitHub repository URL" + None, "--github-repo", "--repo", help="GitHub repository URL" ), subnet_contact: Optional[str] = typer.Option( - None, "--subnet-contact", help="Contact email for subnet" + None, + "--subnet-contact", + "--contact", + "--email", + help="Contact email for subnet", ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, From a162bf4af00de6d45afe595e402718264f4ec204 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 09:19:48 -0800 Subject: [PATCH 191/332] Bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index e0b9ba83..18309767 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc2" +__version__ = "8.2.0rc3" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 87c45828..3b8323c4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc2" +__version__ = "8.2.0rc3" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 7a73b75547af3847972a69543b7b996ea6c3aa83 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 09:36:22 -0800 Subject: [PATCH 192/332] Clamp network calls --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index ab476dff..b01f81a3 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2409,6 +2409,7 @@ def format_cell( registered_delegate_info, dynamic_info, ) = await get_stake_data() + balance = await subtensor.get_balance(coldkey_address) # Iterate over substakes and aggregate them by hotkey. hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} @@ -2539,7 +2540,6 @@ def format_cell( console.print("\nPress Enter to continue to the next hotkey...") input() - balance = await subtensor.get_balance(coldkey_address) total_tao_value = ( f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}" if not verbose From b87906b630325daa148a27831f18a74669ef8a82 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 10:54:29 -0800 Subject: [PATCH 193/332] return when no stake --- bittensor_cli/src/commands/stake/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b01f81a3..e7ce4692 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1201,7 +1201,7 @@ async def unstake_selection( print_error(f"You have no stakes to unstake in subnet {netuid}.") else: print_error("You have no stakes to unstake.") - return + raise typer.Exit() hotkeys_info = [] for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): From e6e59b35b5cf3a6dd8f4da5c8e3f05751ad9edb0 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 11:43:56 -0800 Subject: [PATCH 194/332] Update chain_data for subnet_identityt --- bittensor_cli/src/bittensor/chain_data.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 2bad1020..f5596f1a 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -917,6 +917,7 @@ def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo" @classmethod def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" + netuid = int(decoded["netuid"]) symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() @@ -952,11 +953,14 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": else Balance.from_tao(1) ) # TODO: Patching this temporarily for netuid 0 - subnet_identity = ( - SubnetIdentity.from_vec_u8(decoded["subnet_identity"]) - if decoded.get("subnet_identity") - else None - ) + if decoded.get("subnet_identity"): + subnet_identity = SubnetIdentity( + subnet_name=decoded["subnet_identity"]["subnet_name"], + github_repo=decoded["subnet_identity"]["github_repo"], + subnet_contact=decoded["subnet_identity"]["subnet_contact"], + ) + else: + subnet_identity = None return cls( netuid=netuid, From 6f8e2386d9cd8159c42e453e8d1a5bbc30fd8bd6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 12:40:29 -0800 Subject: [PATCH 195/332] Updates identities for sn names/symbols --- bittensor_cli/src/bittensor/utils.py | 18 ++++++++++++++++++ bittensor_cli/src/commands/stake/stake.py | 5 +++-- bittensor_cli/src/commands/subnets.py | 10 +++++----- bittensor_cli/src/commands/sudo.py | 4 +++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 2a404a69..09c6b251 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1212,3 +1212,21 @@ def is_valid_contact(contact: str) -> bool: """ email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" return bool(re.match(email_pattern, contact)) + + +def get_subnet_name(subnet_info) -> str: + """Get the subnet name, prioritizing subnet_identity.subnet_name over subnet.subnet_name. + + Args: + subnet: The subnet dynamic info + + Returns: + str: The subnet name or empty string if no name is found + """ + return ( + subnet_info.subnet_identity.subnet_name + if hasattr(subnet_info, 'subnet_identity') + and subnet_info.subnet_identity is not None + and subnet_info.subnet_identity.subnet_name is not None + else (subnet_info.subnet_name if subnet_info.subnet_name is not None else "") + ) \ No newline at end of file diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index e7ce4692..2c43a527 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -29,6 +29,7 @@ format_error_message, group_subnets, millify_tao, + get_subnet_name, ) if TYPE_CHECKING: @@ -2180,7 +2181,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): if not verbose else f"{substake_.stake.tao:,.4f}" ) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {SUBNETS.get(netuid, '~')}" + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {get_subnet_name(dynamic_info[netuid])}" rows.append( [ @@ -2371,7 +2372,7 @@ def format_cell( ) subnet_name_cell = ( f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - f" {SUBNETS.get(netuid, '~')}" + f" {get_subnet_name(dynamic_info[netuid])}" ) rows.append( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index a8807592..68a535f9 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -38,6 +38,7 @@ render_table, update_metadata_table, prompt_for_identity, + get_subnet_name, ) if TYPE_CHECKING: @@ -357,7 +358,7 @@ def create_table(subnets, subnet_tao, block_number): netuid_cell = str(netuid) subnet_name_cell = ( f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - f" {SUBNETS.get(netuid, '~')}" + f" {get_subnet_name(subnet)}" ) emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{price_value} τ/{symbol}" @@ -546,7 +547,7 @@ def format_liquidity_cell( netuid_cell = str(netuid) subnet_name_cell = ( f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - f" {SUBNETS.get(netuid, '~')}" + f" {get_subnet_name(subnet)}" ) emission_cell = format_cell( emission_tao, @@ -1063,7 +1064,7 @@ async def show_subnet(netuid_: int): # Define table properties table = Table( title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" - f"{': ' + SUBNETS.get(netuid_, '') if SUBNETS.get(netuid_) else ''}" + f"{': ' + get_subnet_name(subnet_info)}" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, @@ -1233,8 +1234,7 @@ async def show_subnet(netuid_: int): console.print("\n") if not delegate_selection: - subnet_name = SUBNETS.get(netuid_, "") - subnet_name_display = f": {subnet_name}" if subnet_name else "" + subnet_name_display = f": {get_subnet_name(subnet_info)}" tao_pool = ( f"{millify_tao(subnet_info.tao_in.tao)}" if not verbose diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4a9ee5da..e85321fe 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -494,6 +494,8 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): print_error(f"Subnet with netuid {netuid} does not exist.") return False subnet = await subtensor.get_subnet_hyperparameters(netuid) + _subnet_info = await subtensor.get_all_subnet_dynamic_info() + subnet_info = _subnet_info[netuid] table = Table( Column("[white]HYPERPARAMETER", style=COLOR_PALETTE['SUDO']['HYPERPARAMETER']), @@ -501,7 +503,7 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): Column("[white]NORMALIZED", style=COLOR_PALETTE['SUDO']['NORMALIZED']), title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" - f"{f' ({SUBNETS.get(netuid)})' if SUBNETS.get(netuid) else ''}" + f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, From 0ce48ba89a7280aa39a9c8da7dbbad2e86c59d5f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 15:39:20 -0800 Subject: [PATCH 196/332] Various improvements --- bittensor_cli/src/commands/subnets.py | 138 +++++++++++++++++--------- 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 68a535f9..b4e71ff9 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -374,7 +374,7 @@ def create_table(subnets, subnet_tao, block_number): if netuid != 0: tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" else: - tempo_cell = "[red]N/A[/red]" + tempo_cell = "-/-" rows.append( ( @@ -829,6 +829,10 @@ async def show_root(): ) return + tao_sum = sum( + [root_state.tao_stake[idx].tao for idx in range(len(root_state.tao_stake))] + ) + table = Table( title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Root Network\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, @@ -842,23 +846,24 @@ async def show_root(): ) table.add_column("[bold white]Position", style="white", justify="center") - table.add_column( - f"[bold white]Total Stake ({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], - justify="center", - ) + # table.add_column( + # f"[bold white]Total Stake ({Balance.get_unit(0)})", + # style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + # justify="center", + # ) # ------- Temporary columns for testing ------- - table.add_column( - "Alpha (τ)", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], - no_wrap=True, - justify="right", - ) + # table.add_column( + # "Alpha (τ)", + # style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + # no_wrap=True, + # justify="right", + # ) table.add_column( "Tao (τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", + footer=f"{tao_sum:.4f} τ" if verbose else f"{millify_tao(tao_sum)} τ", ) # ------- End Temporary columns for testing ------- table.add_column( @@ -884,7 +889,7 @@ async def show_root(): sorted_hotkeys = sorted( enumerate(root_state.hotkeys), - key=lambda x: root_state.total_stake[x[0]], + key=lambda x: root_state.tao_stake[x[0]], reverse=True, ) sorted_rows = [] @@ -914,12 +919,12 @@ async def show_root(): sorted_rows.append( ( str((pos + 1)), # Position - f"τ {millify_tao(root_state.total_stake[idx].tao)}" - if not verbose - else f"{root_state.total_stake[idx]}", # Total Stake - f"τ {root_state.alpha_stake[idx].tao:.4f}" - if verbose - else f"τ {millify_tao(root_state.alpha_stake[idx])}", # Alpha Stake + # f"τ {millify_tao(root_state.total_stake[idx].tao)}" + # if not verbose + # else f"{root_state.total_stake[idx]}", # Total Stake + # f"τ {root_state.alpha_stake[idx].tao:.4f}" + # if verbose + # else f"τ {millify_tao(root_state.alpha_stake[idx])}", # Alpha Stake f"τ {root_state.tao_stake[idx].tao:.4f}" if verbose else f"τ {millify_tao(root_state.tao_stake[idx])}", # Tao Stake @@ -953,11 +958,6 @@ async def show_root(): if not verbose else f"{root_info.tao_in.tao:,.4f}" ) - alpha_pool = ( - f"{millify_tao(root_info.alpha_in.tao)}" - if not verbose - else f"{root_info.alpha_in.tao:,.4f}" - ) stake = ( f"{millify_tao(root_info.alpha_out.tao)}" if not verbose @@ -970,11 +970,10 @@ async def show_root(): ) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{rate} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {alpha_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {stake}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{rate} τ/τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]τ {stake}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) console.print( @@ -1020,6 +1019,9 @@ async def show_root(): console.print("[red]Please enter a valid number[/red]") async def show_subnet(netuid_: int): + if not await subtensor.subnet_exists(netuid=netuid): + err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + raise typer.Exit() ( _subnet_info, hex_bytes_result, @@ -1036,13 +1038,6 @@ async def show_subnet(netuid_: int): subtensor.get_delegate_identities(), ) subnet_info = _subnet_info[netuid_] - owner_ss58 = subnet_info.owner_coldkey if subnet_info else "" - owner_identity = identities.get(owner_ss58, {}).get( - "name", - old_identities.get(owner_ss58).display - if old_identities.get(owner_ss58) - else "", - ) if (bytes_result := hex_bytes_result) is None: err_console.print(f"Subnet {netuid_} does not exist") @@ -1080,23 +1075,60 @@ async def show_subnet(netuid_: int): if delegate_selection: table.add_column("#", style="cyan", justify="right") - rows = [] + # For hotkey_block_emission calculation emission_sum = sum( [ subnet_state.emission[idx].tao for idx in range(len(subnet_state.emission)) ] ) - tao_sum = Balance(0) + + # For table footers + alpha_sum = sum( + [ + subnet_state.alpha_stake[idx].tao + for idx in range(len(subnet_state.alpha_stake)) + ] + ) + stake_sum = sum( + [ + subnet_state.total_stake[idx].tao + for idx in range(len(subnet_state.total_stake)) + ] + ) + tao_sum = sum( + [ + subnet_state.tao_stake[idx].tao + for idx in range(len(subnet_state.tao_stake)) + ] + ) relative_emissions_sum = 0 owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) + if subnet_info.owner_hotkey not in owner_hotkeys: + owner_hotkeys.append(subnet_info.owner_hotkey) + + owner_identity = identities.get(subnet_info.owner_coldkey, {}).get("name", "") + if not owner_identity: + # If no coldkey identity found, try each owner hotkey + for hotkey in owner_hotkeys: + if hotkey_identity := old_identities.get(hotkey): + owner_identity = hotkey_identity.display + break sorted_indices = sorted( range(len(subnet_state.hotkeys)), - key=lambda i: subnet_state.total_stake[i].tao, - reverse=True, + key=lambda i: ( + # Sort by owner status first + not ( + subnet_state.coldkeys[i] == subnet_info.owner_coldkey + or subnet_state.hotkeys[i] in owner_hotkeys + ), + # Then sort by stake amount (higher stakes first) + -subnet_state.total_stake[i].tao, + ), ) + rows = [] for idx in sorted_indices: hotkey_block_emission = ( subnet_state.emission[idx].tao / emission_sum @@ -1104,7 +1136,6 @@ async def show_subnet(netuid_: int): else 0 ) relative_emissions_sum += hotkey_block_emission - tao_sum += subnet_state.total_stake[idx] # Get identity for this uid coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( @@ -1121,9 +1152,14 @@ async def show_subnet(netuid_: int): subnet_state.coldkeys[idx] == subnet_info.owner_coldkey or subnet_state.hotkeys[idx] in owner_hotkeys ): - uid_identity = ( - f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" - ) + if uid_identity == "~": + uid_identity = ( + f"[dark_sea_green3](*Owner controlled)[/dark_sea_green3]" + ) + else: + uid_identity = ( + f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" + ) rows.append( ( @@ -1159,9 +1195,9 @@ async def show_subnet(netuid_: int): style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", - footer=f"{tao_sum.set_unit(subnet_info.netuid)}" + footer=f"{stake_sum:.4f} {subnet_info.symbol}" if verbose - else f"{millify_tao(tao_sum.tao)} {subnet_info.symbol}", + else f"{millify_tao(stake_sum)} {subnet_info.symbol}", ) # ------- Temporary columns for testing ------- table.add_column( @@ -1169,12 +1205,18 @@ async def show_subnet(netuid_: int): style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", + footer=f"{alpha_sum:.4f} {subnet_info.symbol}" + if verbose + else f"{millify_tao(alpha_sum)} {subnet_info.symbol}", ) table.add_column( - f"Tao (τ)", + "Tao (τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", + footer=f"{tao_sum:.4f} {subnet_info.symbol}" + if verbose + else f"{millify_tao(tao_sum)} {subnet_info.symbol}", ) # ------- End Temporary columns for testing ------- table.add_column( @@ -1234,7 +1276,7 @@ async def show_subnet(netuid_: int): console.print("\n") if not delegate_selection: - subnet_name_display = f": {get_subnet_name(subnet_info)}" + subnet_name_display = f": {get_subnet_name(subnet_info)}" tao_pool = ( f"{millify_tao(subnet_info.tao_in.tao)}" if not verbose From be191ec3df477eb9d24c8c5f258fad9c0b7eeac9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 18:31:55 -0800 Subject: [PATCH 197/332] Updates websocket + bumps version --- .../bittensor/async_substrate_interface.py | 36 +++++++++---------- bittensor_cli/src/commands/subnets.py | 2 +- requirements.txt | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index bd6bbb98..342c2c78 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -4,7 +4,7 @@ from collections import defaultdict from dataclasses import dataclass from hashlib import blake2b -from typing import Optional, Any, Union, Callable, Awaitable, cast +from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 from async_property import async_property @@ -19,7 +19,11 @@ BlockNotFound, ) from substrateinterface.storage import StorageKey -import websockets +from websockets.asyncio.client import connect +from websockets.exceptions import ConnectionClosed + +if TYPE_CHECKING: + from websockets.asyncio.client import ClientConnection ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]] @@ -433,7 +437,7 @@ def add_item( self.block_hashes[block_hash] = runtime def retrieve( - self, block: Optional[int], block_hash: Optional[str] + self, block: Optional[int] = None, block_hash: Optional[str] = None ) -> Optional["Runtime"]: if block is not None: return self.blocks.get(block) @@ -624,7 +628,7 @@ def __init__( # TODO allow setting max concurrent connections and rpc subscriptions per connection # TODO reconnection logic self.ws_url = ws_url - self.ws: Optional[websockets.WebSocketClientProtocol] = None + self.ws: Optional["ClientConnection"] = None self.id = 0 self.max_subscriptions = max_subscriptions self.max_connections = max_connections @@ -646,15 +650,12 @@ async def __aenter__(self): self._exit_task.cancel() if not self._initialized: self._initialized = True - await self._connect() + self.ws = await asyncio.wait_for( + connect(self.ws_url, **self._options), timeout=10 + ) self._receiving_task = asyncio.create_task(self._start_receiving()) return self - async def _connect(self): - self.ws = await asyncio.wait_for( - websockets.connect(self.ws_url, **self._options), timeout=10 - ) - async def __aexit__(self, exc_type, exc_val, exc_tb): async with self._lock: self._in_use -= 1 @@ -695,9 +696,7 @@ async def shutdown(self): async def _recv(self) -> None: try: - response = json.loads( - await cast(websockets.WebSocketClientProtocol, self.ws).recv() - ) + response = json.loads(await self.ws.recv()) async with self._lock: self._open_subscriptions -= 1 if "id" in response: @@ -706,7 +705,7 @@ async def _recv(self) -> None: self._received[response["params"]["subscription"]] = response else: raise KeyError(response) - except websockets.ConnectionClosed: + except ConnectionClosed: raise except KeyError as e: raise e @@ -717,7 +716,7 @@ async def _start_receiving(self): await self._recv() except asyncio.CancelledError: pass - except websockets.ConnectionClosed: + except ConnectionClosed: # TODO try reconnect, but only if it's needed raise @@ -734,7 +733,7 @@ async def send(self, payload: dict) -> int: try: await self.ws.send(json.dumps({**payload, **{"id": original_id}})) return original_id - except websockets.ConnectionClosed: + except ConnectionClosed: raise async def retrieve(self, item_id: int) -> Optional[dict]: @@ -775,7 +774,6 @@ def __init__( chain_endpoint, options={ "max_size": 2**32, - "read_limit": 2**16, "write_limit": 2**16, }, ) @@ -1135,7 +1133,7 @@ async def create_storage_key( ------- StorageKey """ - runtime = await self.init_runtime(block_hash=block_hash) + await self.init_runtime(block_hash=block_hash) return StorageKey.create_from_storage_function( pallet, @@ -1555,7 +1553,7 @@ async def _process_response( self, response: dict, subscription_id: Union[int, str], - value_scale_type: Optional[str], + value_scale_type: Optional[str] = None, storage_item: Optional[ScaleType] = None, runtime: Optional[Runtime] = None, result_handler: Optional[ResultHandler] = None, diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index b4e71ff9..f6ac581f 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -629,7 +629,7 @@ def format_liquidity_cell( tempo_cell = ( (f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}") if netuid != 0 - else "[red]N/A[/red]" + else "-/-" ) rows.append( diff --git a/requirements.txt b/requirements.txt index 25066875..f930e5f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ rich~=13.7 scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 -websockets==13.0 +websockets>=14.1 bittensor-wallet>=2.0.2 bt-decode==0.2.0a0 \ No newline at end of file From 8b0b7757658a894e871a5c90cb4d82f1776772c6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 18:33:34 -0800 Subject: [PATCH 198/332] Bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 18309767..5c33576c 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc3" +__version__ = "8.2.0rc4" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3b8323c4..72a4a751 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc3" +__version__ = "8.2.0rc4" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 11fb7e69ff29f0ac42b8fc7be361f51043f00702 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 7 Jan 2025 18:38:53 -0800 Subject: [PATCH 199/332] Updates bt-decode + bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 5c33576c..37f22487 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc4" +__version__ = "8.2.0rc5" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 72a4a751..bcde742b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc4" +__version__ = "8.2.0rc5" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/requirements.txt b/requirements.txt index f930e5f9..369fa115 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 bittensor-wallet>=2.0.2 -bt-decode==0.2.0a0 \ No newline at end of file +bt-decode==0.4.0 \ No newline at end of file From 99e6ee490446cef569d625af10521bbb29e31343 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 8 Jan 2025 09:54:41 -0800 Subject: [PATCH 200/332] Fix uri utils --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 09c6b251..798bca10 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1015,7 +1015,7 @@ def validate_netuid(value: int) -> int: def validate_uri(uri: str) -> str: if not uri: - raise ValueError("URI cannot be empty") + return None clean_uri = uri.lstrip("/").lower() if not clean_uri.isalnum(): raise typer.BadParameter( From 10026e7540043e70b9564da3589b71c63b317fcb Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 8 Jan 2025 10:58:45 -0800 Subject: [PATCH 201/332] fix root stakiong --- bittensor_cli/src/commands/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index f6ac581f..86905292 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -1004,7 +1004,7 @@ async def show_root(): if 1 <= idx <= max_rows: selected_hotkey = sorted_hks_delegation[idx - 1] row_data = sorted_rows[idx - 1] - identity = "" if row_data[7] == "~" else row_data[7] + identity = "" if row_data[5] == "~" else row_data[5] identity_str = f" ({identity})" if identity else "" console.print( f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" From 524d7e986a9fd1228fdee8a61238025218709e02 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 8 Jan 2025 15:36:21 -0800 Subject: [PATCH 202/332] stake_weight added + metagraph aliased --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/subnets.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bcde742b..f7e4ec13 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -748,7 +748,7 @@ def __init__(self): )(self.subnets_register) self.subnets_app.command( "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True - )(self.subnets_metagraph) + )(self.subnets_show) #Aliased to `s show` for now self.subnets_app.command( "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 86905292..39cbd716 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -44,6 +44,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +TAO_WEIGHT = 0.018 # helpers and extrinsics @@ -1098,7 +1099,7 @@ async def show_subnet(netuid_: int): ) tao_sum = sum( [ - subnet_state.tao_stake[idx].tao + subnet_state.tao_stake[idx].tao * TAO_WEIGHT for idx in range(len(subnet_state.tao_stake)) ] ) @@ -1161,6 +1162,8 @@ async def show_subnet(netuid_: int): f"[dark_sea_green3]{uid_identity} (*Owner)[/dark_sea_green3]" ) + # Modify tao stake with TAO_WEIGHT + tao_stake = subnet_state.tao_stake[idx] * TAO_WEIGHT rows.append( ( str(idx), # UID @@ -1170,9 +1173,7 @@ async def show_subnet(netuid_: int): f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake - f"τ {subnet_state.tao_stake[idx].tao:.4f}" - if verbose - else f"τ {millify_tao(subnet_state.tao_stake[idx])}", # Tao Stake + f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends f"{subnet_state.incentives[idx]:.4f}", # Incentive From 957cd0366a27fa6c83209258229981f8a8801ffa Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 09:26:39 -0800 Subject: [PATCH 203/332] Update using custom errors --- bittensor_cli/src/bittensor/utils.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 798bca10..869116fd 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -508,22 +508,8 @@ def format_error_message( err_data = error_message.get("data", "") # subtensor custom error marker - if err_data.startswith("Custom error:") and substrate: - if substrate.metadata: - try: - pallet = substrate.metadata.get_metadata_pallet( - "SubtensorModule" - ) - error_index = int(err_data.split("Custom error:")[-1]) - - error_dict = pallet.errors[error_index].value - err_type = error_dict.get("message", err_type) - err_docs = error_dict.get("docs", []) - err_description = err_docs[0] if err_docs else err_description - except (AttributeError, IndexError): - err_console.print( - "Substrate pallets data unavailable. This is usually caused by an uninitialized substrate." - ) + if err_data.startswith("Custom error:"): + err_description = f"{err_data} | Please consult https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages" else: err_description = err_data From 5cfb02c271847fdac269cdb8b32bd727b77d0200 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 09:44:40 -0800 Subject: [PATCH 204/332] Reduce 1 call --- bittensor_cli/src/commands/subnets.py | 33 ++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 39cbd716..e0ef2b94 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -201,10 +201,7 @@ async def subnets_list( async def fetch_subnet_data(): block_number = await subtensor.substrate.get_block_number(None) - subnets, subnet_tao = await asyncio.gather( - subtensor.get_all_subnet_dynamic_info(), - subtensor.get_subnet_tao(), - ) + subnets = await subtensor.get_all_subnet_dynamic_info() # Sort subnets by market cap, keeping the root subnet in the first position root_subnet = next(s for s in subnets if s.netuid == 0) @@ -214,24 +211,24 @@ async def fetch_subnet_data(): reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, subnet_tao, block_number + return sorted_subnets, block_number def calculate_emission_stats( - subnet_tao: dict, block_number: int + subnets: list, block_number: int ) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation total_tao_emitted = sum( - subnet_tao.get(n, Balance(0)) for n in subnet_tao.keys() if n != 0 + subnet.tao_in.tao for subnet in subnets if subnet.netuid != 0 ) - emission_percentage = (total_tao_emitted.tao / block_number) * 100 + emission_percentage = (total_tao_emitted / block_number) * 100 percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" formatted_percentage = ( f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" ) if not verbose: - percentage_string = f"τ {millify_tao(total_tao_emitted.tao)}/{millify_tao(block_number)} ({formatted_percentage})" + percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number)} ({formatted_percentage})" else: - percentage_string = f"τ {total_tao_emitted.tao:,.1f}/{block_number} ({formatted_percentage})" + percentage_string = f"τ {total_tao_emitted:.1f}/{block_number} ({formatted_percentage})" return total_tao_emitted, percentage_string def define_table( @@ -303,9 +300,9 @@ def define_table( return table # Non-live mode - def create_table(subnets, subnet_tao, block_number): + def create_table(subnets, block_number): rows = [] - _, percentage_string = calculate_emission_stats(subnet_tao, block_number) + _, percentage_string = calculate_emission_stats(subnets, block_number) for subnet in subnets: netuid = subnet.netuid @@ -408,7 +405,7 @@ def create_table(subnets, subnet_tao, block_number): return table # Live mode - def create_table_live(subnets, previous_data, subnet_tao, block_number): + def create_table_live(subnets, previous_data, block_number): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -512,7 +509,7 @@ def format_liquidity_cell( rows = [] current_data = {} # To store current values for comparison in the next update - _, percentage_string = calculate_emission_stats(subnet_tao, block_number) + _, percentage_string = calculate_emission_stats(subnets, block_number) for subnet in subnets: netuid = subnet.netuid @@ -692,7 +689,7 @@ def format_liquidity_cell( with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, subnet_tao, block_number = await fetch_subnet_data() + subnets, block_number = await fetch_subnet_data() # Update block numbers previous_block = current_block @@ -704,7 +701,7 @@ def format_liquidity_cell( ) table, current_data = create_table_live( - subnets, previous_data, subnet_tao, block_number + subnets, previous_data, block_number ) previous_data = current_data progress.reset(progress_task) @@ -730,8 +727,8 @@ def format_liquidity_cell( pass # Ctrl + C else: # Non-live mode - subnets, subnet_tao, block_number = await fetch_subnet_data() - table = create_table(subnets, subnet_tao, block_number) + subnets, block_number = await fetch_subnet_data() + table = create_table(subnets, block_number) console.print(table) return From e3813843b0c8c4d5bbe45f1ff86b5032490ce1e4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 10:09:14 -0800 Subject: [PATCH 205/332] patches --all in unstaking --- bittensor_cli/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f7e4ec13..bb78a7f3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2728,12 +2728,14 @@ def stake_remove( False, "--unstake-all", "--all", + hidden=True, help="When set, this command unstakes all staked TAO + Alpha from the all hotkeys.", ), unstake_all_alpha: bool = typer.Option( False, "--unstake-all-alpha", "--all-alpha", + hidden=True, help="When set, this command unstakes all staked Alpha from the all hotkeys.", ), amount: float = typer.Option( @@ -2789,6 +2791,9 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) + # TODO: Coldkey related unstakes need to be updated. Patching for now. + unstake_all_alpha = False + unstake_all = False if interactive and any( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] From 98d9dee9b39d0aa120ede8ec6ce234badad31618 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 10:24:40 -0800 Subject: [PATCH 206/332] Bumps version, updates endpoint --- CHANGELOG.md | 5 +++++ README.md | 1 + bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- bittensor_cli/src/__init__.py | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ae097d..6488bc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc6 /2025-01-09 + +## What's Changed +* RAO development version for the rao chain + ## 8.2.0 /2024-10-10 ## What's Changed diff --git a/README.md b/README.md index bd71dfe5..1d8fcc0d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@
# Bittensor CLI +### Rao Development Version [![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI version](https://badge.fury.io/py/bittensor_cli.svg)](https://badge.fury.io/py/bittensor_cli) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 37f22487..efcab110 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc5" +__version__ = "8.2.0rc6" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bb78a7f3..7488a4f5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc5" +__version__ = "8.2.0rc6" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1627db0c..88da5271 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -97,7 +97,7 @@ class config: } class subtensor: - network = "rao" + network = "test" chain_endpoint = None _mock = False From 532c05203a25886a9373a59b6402d2238eb7b36f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 11:00:20 -0800 Subject: [PATCH 207/332] Updates units --- bittensor_cli/src/__init__.py | 532 +++++++++++++++++++++++++++------- 1 file changed, 432 insertions(+), 100 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 88da5271..09311d0e 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -343,6 +343,7 @@ class WalletValidationTypes(Enum): } UNITS = [ + # Greek Alphabet (0-24) "\u03c4", # τ (tau, 0) "\u03b1", # α (alpha, 1) "\u03b2", # β (beta, 2) @@ -368,7 +369,7 @@ class WalletValidationTypes(Enum): "\u03c7", # χ (chi, 22) "\u03c8", # ψ (psi, 23) "\u03c9", # ω (omega, 24) - # Hebrew letters + # Hebrew Alphabet (25-51) "\u05d0", # א (aleph, 25) "\u05d1", # ב (bet, 26) "\u05d2", # ג (gimel, 27) @@ -396,47 +397,8 @@ class WalletValidationTypes(Enum): "\u05e8", # ר (resh, 49) "\u05e9", # ש (shin, 50) "\u05ea", # ת (tav, 51) - # Georgian Alphabet (Mkhedruli) - "\u10d0", # ა (Ani, 97) - "\u10d1", # ბ (Bani, 98) - "\u10d2", # გ (Gani, 99) - "\u10d3", # დ (Doni, 100) - "\u10d4", # ე (Eni, 101) - "\u10d5", # ვ (Vini, 102) - # Armenian Alphabet - "\u0531", # Ա (Ayp, 103) - "\u0532", # Բ (Ben, 104) - "\u0533", # Գ (Gim, 105) - "\u0534", # Դ (Da, 106) - "\u0535", # Ե (Ech, 107) - "\u0536", # Զ (Za, 108) - # "\u055e", # ՞ (Question mark, 109) - # Runic Alphabet - "\u16a0", # ᚠ (Fehu, wealth, 81) - "\u16a2", # ᚢ (Uruz, strength, 82) - "\u16a6", # ᚦ (Thurisaz, giant, 83) - "\u16a8", # ᚨ (Ansuz, god, 84) - "\u16b1", # ᚱ (Raidho, ride, 85) - "\u16b3", # ᚲ (Kaunan, ulcer, 86) - "\u16c7", # ᛇ (Eihwaz, yew, 87) - "\u16c9", # ᛉ (Algiz, protection, 88) - "\u16d2", # ᛒ (Berkanan, birch, 89) - # Cyrillic Alphabet - "\u0400", # Ѐ (Ie with grave, 110) - "\u0401", # Ё (Io, 111) - "\u0402", # Ђ (Dje, 112) - "\u0403", # Ѓ (Gje, 113) - "\u0404", # Є (Ukrainian Ie, 114) - "\u0405", # Ѕ (Dze, 115) - # Coptic Alphabet - "\u2c80", # Ⲁ (Alfa, 116) - "\u2c81", # ⲁ (Small Alfa, 117) - "\u2c82", # Ⲃ (Vida, 118) - "\u2c83", # ⲃ (Small Vida, 119) - "\u2c84", # Ⲅ (Gamma, 120) - "\u2c85", # ⲅ (Small Gamma, 121) - # Arabic letters - "\u0627", # ا (alef, 52) + # Arabic Alphabet (52-81) + "\u0627", # ا (alif, 52) "\u0628", # ب (ba, 53) "\u062a", # ت (ta, 54) "\u062b", # ث (tha, 55) @@ -463,19 +425,389 @@ class WalletValidationTypes(Enum): "\u0646", # ن (noon, 76) "\u0647", # ه (ha, 77) "\u0648", # و (waw, 78) - "\u0649", # ى (alef maksura, 79) - "\u064a", # ي (ya, 80) - # Ogham Alphabet - "\u1680", #   (Space, 90) - "\u1681", # ᚁ (Beith, birch, 91) - "\u1682", # ᚂ (Luis, rowan, 92) - "\u1683", # ᚃ (Fearn, alder, 93) - "\u1684", # ᚄ (Sail, willow, 94) - "\u1685", # ᚅ (Nion, ash, 95) - "\u169b", # ᚛ (Forfeda, 96) - # Tifinagh Alphabet - "\u2d30", # ⴰ (Ya, 127) - "\u2d31", # ⴱ (Yab, 128) + "\u064a", # ي (ya, 79) + "\u0649", # ى (alef maksura, 80) + "\u064a", # ي (ya, 81) + # Runic Alphabet (82-90) + "\u16a0", # ᚠ (fehu, 82) + "\u16a2", # ᚢ (uruz, 83) + "\u16a6", # ᚦ (thurisaz, 84) + "\u16a8", # ᚨ (ansuz, 85) + "\u16b1", # ᚱ (raidho, 86) + "\u16b3", # ᚲ (kaunan, 87) + "\u16c7", # ᛇ (eihwaz, 88) + "\u16c9", # ᛉ (algiz, 89) + "\u16d2", # ᛒ (berkanan, 90) + # Ogham Alphabet (91-97) + "\u1680", #   (Space, 91) + "\u1681", # ᚁ (Beith, 92) + "\u1682", # ᚂ (Luis, 93) + "\u1683", # ᚃ (Fearn, 94) + "\u1684", # ᚄ (Sail, 95) + "\u1685", # ᚅ (Nion, 96) + "\u169b", # ᚛ (Forfeda, 97) + # Georgian Alphabet (98-103) + "\u10d0", # ა (ani, 98) + "\u10d1", # ბ (bani, 99) + "\u10d2", # გ (gani, 100) + "\u10d3", # დ (doni, 101) + "\u10d4", # ე (eni, 102) + "\u10d5", # ვ (vini, 103) + # Armenian Alphabet (104-110) + "\u0531", # Ա (Ayp, 104) + "\u0532", # Բ (Ben, 105) + "\u0533", # Գ (Gim, 106) + "\u0534", # Դ (Da, 107) + "\u0535", # Ե (Ech, 108) + "\u0536", # Զ (Za, 109) + "\u055e", # ՞ (Question mark, 110) + # Cyrillic Alphabet (111-116) + "\u0400", # Ѐ (Ie with grave, 111) + "\u0401", # Ё (Io, 112) + "\u0402", # Ђ (Dje, 113) + "\u0403", # Ѓ (Gje, 114) + "\u0404", # Є (Ukrainian Ie, 115) + "\u0405", # Ѕ (Dze, 116) + # Coptic Alphabet (117-122) + "\u2c80", # Ⲁ (Alfa, 117) + "\u2c81", # ⲁ (Small Alfa, 118) + "\u2c82", # Ⲃ (Vida, 119) + "\u2c83", # ⲃ (Small Vida, 120) + "\u2c84", # Ⲅ (Gamma, 121) + "\u2c85", # ⲅ (Small Gamma, 122) + # Brahmi Script (123-127) + "\U00011000", # 𑀀 (A, 123) + "\U00011001", # 𑀁 (Aa, 124) + "\U00011002", # 𑀂 (I, 125) + "\U00011003", # 𑀃 (Ii, 126) + "\U00011005", # 𑀅 (U, 127) + # Tifinagh Alphabet (128-133) + "\u2d30", # ⴰ (Ya, 128) + "\u2d31", # ⴱ (Yab, 129) + "\u2d32", # ⴲ (Yabh, 130) + "\u2d33", # ⴳ (Yag, 131) + "\u2d34", # ⴴ (Yagh, 132) + "\u2d35", # ⴵ (Yaj, 133) + # Glagolitic Alphabet (134-166) + "\u2c00", # Ⰰ (Az, 134) + "\u2c01", # Ⰱ (Buky, 135) + "\u2c02", # Ⰲ (Vede, 136) + "\u2c03", # Ⰳ (Glagoli, 137) + "\u2c04", # Ⰴ (Dobro, 138) + "\u2c05", # Ⰵ (Yest, 139) + "\u2c06", # Ⰶ (Zhivete, 140) + "\u2c07", # Ⰷ (Zemlja, 141) + "\u2c08", # Ⰸ (Izhe, 142) + "\u2c09", # Ⰹ (Initial Izhe, 143) + "\u2c0a", # Ⰺ (I, 144) + "\u2c0b", # Ⰻ (Djerv, 145) + "\u2c0c", # Ⰼ (Kako, 146) + "\u2c0d", # Ⰽ (Ljudije, 147) + "\u2c0e", # Ⰾ (Myse, 148) + "\u2c0f", # Ⰿ (Nash, 149) + "\u2c10", # Ⱀ (On, 150) + "\u2c11", # Ⱁ (Pokoj, 151) + "\u2c12", # Ⱂ (Rtsy, 152) + "\u2c13", # Ⱃ (Slovo, 153) + "\u2c14", # Ⱄ (Tvrido, 154) + "\u2c15", # Ⱅ (Uku, 155) + "\u2c16", # Ⱆ (Fert, 156) + "\u2c17", # Ⱇ (Xrivi, 157) + "\u2c18", # Ⱈ (Ot, 158) + "\u2c19", # Ⱉ (Cy, 159) + "\u2c1a", # Ⱊ (Shcha, 160) + "\u2c1b", # Ⱋ (Er, 161) + "\u2c1c", # Ⱌ (Yeru, 162) + "\u2c1d", # Ⱍ (Small Yer, 163) + "\u2c1e", # Ⱎ (Yo, 164) + "\u2c1f", # Ⱏ (Yu, 165) + "\u2c20", # Ⱐ (Ja, 166) + # Thai Alphabet (167-210) + "\u0e01", # ก (Ko Kai, 167) + "\u0e02", # ข (Kho Khai, 168) + "\u0e03", # ฃ (Kho Khuat, 169) + "\u0e04", # ค (Kho Khon, 170) + "\u0e05", # ฅ (Kho Rakhang, 171) + "\u0e06", # ฆ (Kho Khwai, 172) + "\u0e07", # ง (Ngo Ngu, 173) + "\u0e08", # จ (Cho Chan, 174) + "\u0e09", # ฉ (Cho Ching, 175) + "\u0e0a", # ช (Cho Chang, 176) + "\u0e0b", # ซ (So So, 177) + "\u0e0c", # ฌ (Cho Choe, 178) + "\u0e0d", # ญ (Yo Ying, 179) + "\u0e0e", # ฎ (Do Chada, 180) + "\u0e0f", # ฏ (To Patak, 181) + "\u0e10", # ฐ (Tho Than, 182) + "\u0e11", # ฑ (Tho Nangmontho, 183) + "\u0e12", # ฒ (Tho Phuthao, 184) + "\u0e13", # ณ (No Nen, 185) + "\u0e14", # ด (Do Dek, 186) + "\u0e15", # ต (To Tao, 187) + "\u0e16", # ถ (Tho Thung, 188) + "\u0e17", # ท (Tho Thahan, 189) + "\u0e18", # ธ (Tho Thong, 190) + "\u0e19", # น (No Nu, 191) + "\u0e1a", # บ (Bo Baimai, 192) + "\u0e1b", # ป (Po Pla, 193) + "\u0e1c", # ผ (Pho Phung, 194) + "\u0e1d", # ฝ (Fo Fa, 195) + "\u0e1e", # พ (Pho Phan, 196) + "\u0e1f", # ฟ (Fo Fan, 197) + "\u0e20", # ภ (Pho Samphao, 198) + "\u0e21", # ม (Mo Ma, 199) + "\u0e22", # ย (Yo Yak, 200) + "\u0e23", # ร (Ro Rua, 201) + "\u0e25", # ล (Lo Ling, 202) + "\u0e27", # ว (Wo Waen, 203) + "\u0e28", # ศ (So Sala, 204) + "\u0e29", # ษ (So Rusi, 205) + "\u0e2a", # ส (So Sua, 206) + "\u0e2b", # ห (Ho Hip, 207) + "\u0e2c", # ฬ (Lo Chula, 208) + "\u0e2d", # อ (O Ang, 209) + "\u0e2e", # ฮ (Ho Nokhuk, 210) + # Hangul Consonants (211-224) + "\u1100", # ㄱ (Giyeok, 211) + "\u1101", # ㄴ (Nieun, 212) + "\u1102", # ㄷ (Digeut, 213) + "\u1103", # ㄹ (Rieul, 214) + "\u1104", # ㅁ (Mieum, 215) + "\u1105", # ㅂ (Bieup, 216) + "\u1106", # ㅅ (Siot, 217) + "\u1107", # ㅇ (Ieung, 218) + "\u1108", # ㅈ (Jieut, 219) + "\u1109", # ㅊ (Chieut, 220) + "\u110a", # ㅋ (Kieuk, 221) + "\u110b", # ㅌ (Tieut, 222) + "\u110c", # ㅍ (Pieup, 223) + "\u110d", # ㅎ (Hieut, 224) + # Hangul Vowels (225-245) + "\u1161", # ㅏ (A, 225) + "\u1162", # ㅐ (Ae, 226) + "\u1163", # ㅑ (Ya, 227) + "\u1164", # ㅒ (Yae, 228) + "\u1165", # ㅓ (Eo, 229) + "\u1166", # ㅔ (E, 230) + "\u1167", # ㅕ (Yeo, 231) + "\u1168", # ㅖ (Ye, 232) + "\u1169", # ㅗ (O, 233) + "\u116a", # ㅘ (Wa, 234) + "\u116b", # ㅙ (Wae, 235) + "\u116c", # ㅚ (Oe, 236) + "\u116d", # ㅛ (Yo, 237) + "\u116e", # ㅜ (U, 238) + "\u116f", # ㅝ (Weo, 239) + "\u1170", # ㅞ (We, 240) + "\u1171", # ㅟ (Wi, 241) + "\u1172", # ㅠ (Yu, 242) + "\u1173", # ㅡ (Eu, 243) + "\u1174", # ㅢ (Ui, 244) + "\u1175", # ㅣ (I, 245) + # Ethiopic Alphabet (246-274) + "\u12a0", # አ (Glottal A, 246) + "\u12a1", # ኡ (Glottal U, 247) + "\u12a2", # ኢ (Glottal I, 248) + "\u12a3", # ኣ (Glottal Aa, 249) + "\u12a4", # ኤ (Glottal E, 250) + "\u12a5", # እ (Glottal Ie, 251) + "\u12a6", # ኦ (Glottal O, 252) + "\u12a7", # ኧ (Glottal Wa, 253) + "\u12c8", # ወ (Wa, 254) + "\u12c9", # ዉ (Wu, 255) + "\u12ca", # ዊ (Wi, 256) + "\u12cb", # ዋ (Waa, 257) + "\u12cc", # ዌ (We, 258) + "\u12cd", # ው (Wye, 259) + "\u12ce", # ዎ (Wo, 260) + "\u12b0", # ኰ (Ko, 261) + "\u12b1", # ኱ (Ku, 262) + "\u12b2", # ኲ (Ki, 263) + "\u12b3", # ኳ (Kua, 264) + "\u12b4", # ኴ (Ke, 265) + "\u12b5", # ኵ (Kwe, 266) + "\u12b6", # ኶ (Ko, 267) + "\u12a0", # ጐ (Go, 268) + "\u12a1", # ጑ (Gu, 269) + "\u12a2", # ጒ (Gi, 270) + "\u12a3", # መ (Gua, 271) + "\u12a4", # ጔ (Ge, 272) + "\u12a5", # ጕ (Gwe, 273) + "\u12a6", # ጖ (Go, 274) + # Devanagari Alphabet (275-318) + "\u0905", # अ (A, 275) + "\u0906", # आ (Aa, 276) + "\u0907", # इ (I, 277) + "\u0908", # ई (Ii, 278) + "\u0909", # उ (U, 279) + "\u090a", # ऊ (Uu, 280) + "\u090b", # ऋ (R, 281) + "\u090f", # ए (E, 282) + "\u0910", # ऐ (Ai, 283) + "\u0913", # ओ (O, 284) + "\u0914", # औ (Au, 285) + "\u0915", # क (Ka, 286) + "\u0916", # ख (Kha, 287) + "\u0917", # ग (Ga, 288) + "\u0918", # घ (Gha, 289) + "\u0919", # ङ (Nga, 290) + "\u091a", # च (Cha, 291) + "\u091b", # छ (Chha, 292) + "\u091c", # ज (Ja, 293) + "\u091d", # झ (Jha, 294) + "\u091e", # ञ (Nya, 295) + "\u091f", # ट (Ta, 296) + "\u0920", # ठ (Tha, 297) + "\u0921", # ड (Da, 298) + "\u0922", # ढ (Dha, 299) + "\u0923", # ण (Na, 300) + "\u0924", # त (Ta, 301) + "\u0925", # थ (Tha, 302) + "\u0926", # द (Da, 303) + "\u0927", # ध (Dha, 304) + "\u0928", # न (Na, 305) + "\u092a", # प (Pa, 306) + "\u092b", # फ (Pha, 307) + "\u092c", # ब (Ba, 308) + "\u092d", # भ (Bha, 309) + "\u092e", # म (Ma, 310) + "\u092f", # य (Ya, 311) + "\u0930", # र (Ra, 312) + "\u0932", # ल (La, 313) + "\u0935", # व (Va, 314) + "\u0936", # श (Sha, 315) + "\u0937", # ष (Ssa, 316) + "\u0938", # स (Sa, 317) + "\u0939", # ह (Ha, 318) + # Katakana Alphabet (319-364) + "\u30a2", # ア (A, 319) + "\u30a4", # イ (I, 320) + "\u30a6", # ウ (U, 321) + "\u30a8", # エ (E, 322) + "\u30aa", # オ (O, 323) + "\u30ab", # カ (Ka, 324) + "\u30ad", # キ (Ki, 325) + "\u30af", # ク (Ku, 326) + "\u30b1", # ケ (Ke, 327) + "\u30b3", # コ (Ko, 328) + "\u30b5", # サ (Sa, 329) + "\u30b7", # シ (Shi, 330) + "\u30b9", # ス (Su, 331) + "\u30bb", # セ (Se, 332) + "\u30bd", # ソ (So, 333) + "\u30bf", # タ (Ta, 334) + "\u30c1", # チ (Chi, 335) + "\u30c4", # ツ (Tsu, 336) + "\u30c6", # テ (Te, 337) + "\u30c8", # ト (To, 338) + "\u30ca", # ナ (Na, 339) + "\u30cb", # ニ (Ni, 340) + "\u30cc", # ヌ (Nu, 341) + "\u30cd", # ネ (Ne, 342) + "\u30ce", # ノ (No, 343) + "\u30cf", # ハ (Ha, 344) + "\u30d2", # ヒ (Hi, 345) + "\u30d5", # フ (Fu, 346) + "\u30d8", # ヘ (He, 347) + "\u30db", # ホ (Ho, 348) + "\u30de", # マ (Ma, 349) + "\u30df", # ミ (Mi, 350) + "\u30e0", # ム (Mu, 351) + "\u30e1", # メ (Me, 352) + "\u30e2", # モ (Mo, 353) + "\u30e4", # ヤ (Ya, 354) + "\u30e6", # ユ (Yu, 355) + "\u30e8", # ヨ (Yo, 356) + "\u30e9", # ラ (Ra, 357) + "\u30ea", # リ (Ri, 358) + "\u30eb", # ル (Ru, 359) + "\u30ec", # レ (Re, 360) + "\u30ed", # ロ (Ro, 361) + "\u30ef", # ワ (Wa, 362) + "\u30f2", # ヲ (Wo, 363) + "\u30f3", # ン (N, 364) + # Tifinagh Alphabet (365-400) + "\u2d30", # ⴰ (Ya, 365) + "\u2d31", # ⴱ (Yab, 366) + "\u2d32", # ⴲ (Yabh, 367) + "\u2d33", # ⴳ (Yag, 368) + "\u2d34", # ⴴ (Yagh, 369) + "\u2d35", # ⴵ (Yaj, 370) + "\u2d36", # ⴶ (Yach, 371) + "\u2d37", # ⴷ (Yad, 372) + "\u2d38", # ⴸ (Yadh, 373) + "\u2d39", # ⴹ (Yadh, emphatic, 374) + "\u2d3a", # ⴺ (Yaz, 375) + "\u2d3b", # ⴻ (Yazh, 376) + "\u2d3c", # ⴼ (Yaf, 377) + "\u2d3d", # ⴽ (Yak, 378) + "\u2d3e", # ⴾ (Yak, variant, 379) + "\u2d3f", # ⴿ (Yaq, 380) + "\u2d40", # ⵀ (Yah, 381) + "\u2d41", # ⵁ (Yahh, 382) + "\u2d42", # ⵂ (Yahl, 383) + "\u2d43", # ⵃ (Yahm, 384) + "\u2d44", # ⵄ (Yayn, 385) + "\u2d45", # ⵅ (Yakh, 386) + "\u2d46", # ⵆ (Yakl, 387) + "\u2d47", # ⵇ (Yahq, 388) + "\u2d48", # ⵈ (Yash, 389) + "\u2d49", # ⵉ (Yi, 390) + "\u2d4a", # ⵊ (Yij, 391) + "\u2d4b", # ⵋ (Yizh, 392) + "\u2d4c", # ⵌ (Yink, 393) + "\u2d4d", # ⵍ (Yal, 394) + "\u2d4e", # ⵎ (Yam, 395) + "\u2d4f", # ⵏ (Yan, 396) + "\u2d50", # ⵐ (Yang, 397) + "\u2d51", # ⵑ (Yany, 398) + "\u2d52", # ⵒ (Yap, 399) + "\u2d53", # ⵓ (Yu, 400) + # Sinhala Alphabet (401-444) + "\u0d85", # අ (A, 401) + "\u0d86", # ආ (Aa, 402) + "\u0d87", # ඉ (I, 403) + "\u0d88", # ඊ (Ii, 404) + "\u0d89", # උ (U, 405) + "\u0d8a", # ඌ (Uu, 406) + "\u0d8b", # ඍ (R, 407) + "\u0d8c", # ඎ (Rr, 408) + "\u0d8f", # ඏ (L, 409) + "\u0d90", # ඐ (Ll, 410) + "\u0d91", # එ (E, 411) + "\u0d92", # ඒ (Ee, 412) + "\u0d93", # ඓ (Ai, 413) + "\u0d94", # ඔ (O, 414) + "\u0d95", # ඕ (Oo, 415) + "\u0d96", # ඖ (Au, 416) + "\u0d9a", # ක (Ka, 417) + "\u0d9b", # ඛ (Kha, 418) + "\u0d9c", # ග (Ga, 419) + "\u0d9d", # ඝ (Gha, 420) + "\u0d9e", # ඞ (Nga, 421) + "\u0d9f", # ච (Cha, 422) + "\u0da0", # ඡ (Chha, 423) + "\u0da1", # ජ (Ja, 424) + "\u0da2", # ඣ (Jha, 425) + "\u0da3", # ඤ (Nya, 426) + "\u0da4", # ට (Ta, 427) + "\u0da5", # ඥ (Tha, 428) + "\u0da6", # ඦ (Da, 429) + "\u0da7", # ට (Dha, 430) + "\u0da8", # ඨ (Na, 431) + "\u0daa", # ඪ (Pa, 432) + "\u0dab", # ණ (Pha, 433) + "\u0dac", # ඬ (Ba, 434) + "\u0dad", # ත (Bha, 435) + "\u0dae", # ථ (Ma, 436) + "\u0daf", # ද (Ya, 437) + "\u0db0", # ධ (Ra, 438) + "\u0db1", # ඲ (La, 439) + "\u0db2", # ඳ (Va, 440) + "\u0db3", # ප (Sha, 441) + "\u0db4", # ඵ (Ssa, 442) + "\u0db5", # බ (Sa, 443) + "\u0db6", # භ (Ha, 444) ] NETWORK_EXPLORER_MAP = { @@ -553,60 +885,60 @@ class WalletValidationTypes(Enum): COLOR_PALETTE = { "GENERAL": { - "HEADER": "#4196D6", # Light Blue - "LINKS": "#8CB9E9", # Sky Blue - "HINT": "#A2E5B8", # Mint Green - "COLDKEY": "#9EF5E4", # Aqua - "HOTKEY": "#ECC39D", # Light Orange/Peach - "SUBHEADING_MAIN": "#7ECFEC", # Light Cyan - "SUBHEADING": "#AFEFFF", # Pale Blue - "SUBHEADING_EXTRA_1": "#96A3C5", # Grayish Blue - "SUBHEADING_EXTRA_2": "#6D7BAF", # Slate Blue - "CONFIRMATION_Y_N_Q": "#EE8DF8", # Light Purple/Pink - "SYMBOL": "#E7CC51", # Gold - "BALANCE": "#4F91C6", # Medium Blue - "COST": "#53B5A0", # Teal - "SUCCESS": "#53B5A0", # Teal - "NETUID": "#CBA880", # Tan - "NETUID_EXTRA": "#DDD5A9", # Light Khaki - "TEMPO": "#67A3A5", # Grayish Teal + "HEADER": "#4196D6", # Light Blue + "LINKS": "#8CB9E9", # Sky Blue + "HINT": "#A2E5B8", # Mint Green + "COLDKEY": "#9EF5E4", # Aqua + "HOTKEY": "#ECC39D", # Light Orange/Peach + "SUBHEADING_MAIN": "#7ECFEC", # Light Cyan + "SUBHEADING": "#AFEFFF", # Pale Blue + "SUBHEADING_EXTRA_1": "#96A3C5", # Grayish Blue + "SUBHEADING_EXTRA_2": "#6D7BAF", # Slate Blue + "CONFIRMATION_Y_N_Q": "#EE8DF8", # Light Purple/Pink + "SYMBOL": "#E7CC51", # Gold + "BALANCE": "#4F91C6", # Medium Blue + "COST": "#53B5A0", # Teal + "SUCCESS": "#53B5A0", # Teal + "NETUID": "#CBA880", # Tan + "NETUID_EXTRA": "#DDD5A9", # Light Khaki + "TEMPO": "#67A3A5", # Grayish Teal }, "STAKE": { - "STAKE_AMOUNT": "#53B5A0", # Teal - "STAKE_ALPHA": "#53B5A0", # Teal - "STAKE_SWAP": "#67A3A5", # Grayish Teal - "TAO": "#4F91C6", # Medium Blue - "SLIPPAGE_TEXT": "#C25E7C", # Rose - "SLIPPAGE_PERCENT": "#E7B195", # Light Coral - "NOT_REGISTERED": "#EB6A6C", # Salmon Red - "EXTRA_1": "#D781BB", # Pink + "STAKE_AMOUNT": "#53B5A0", # Teal + "STAKE_ALPHA": "#53B5A0", # Teal + "STAKE_SWAP": "#67A3A5", # Grayish Teal + "TAO": "#4F91C6", # Medium Blue + "SLIPPAGE_TEXT": "#C25E7C", # Rose + "SLIPPAGE_PERCENT": "#E7B195", # Light Coral + "NOT_REGISTERED": "#EB6A6C", # Salmon Red + "EXTRA_1": "#D781BB", # Pink }, "POOLS": { - "TAO": "#4F91C6", # Medium Blue - "ALPHA_IN": "#D09FE9", # Light Purple - "ALPHA_OUT": "#AB7CC8", # Medium Purple - "RATE": "#F8D384", # Light Orange - "TAO_EQUIV": "#8CB9E9", # Sky Blue - "EMISSION": "#F8D384", # Light Orange - "EXTRA_1": "#CAA8FB", # Lavender - "EXTRA_2": "#806DAF", # Dark Purple + "TAO": "#4F91C6", # Medium Blue + "ALPHA_IN": "#D09FE9", # Light Purple + "ALPHA_OUT": "#AB7CC8", # Medium Purple + "RATE": "#F8D384", # Light Orange + "TAO_EQUIV": "#8CB9E9", # Sky Blue + "EMISSION": "#F8D384", # Light Orange + "EXTRA_1": "#CAA8FB", # Lavender + "EXTRA_2": "#806DAF", # Dark Purple }, "GREY": { - "GREY_100": "#F8F9FA", # Almost White - "GREY_200": "#F1F3F4", # Very Light Grey - "GREY_300": "#DBDDE1", # Light Grey - "GREY_400": "#BDC1C6", # Medium Light Grey - "GREY_500": "#5F6368", # Medium Grey - "GREY_600": "#2E3134", # Medium Dark Grey - "GREY_700": "#282A2D", # Dark Grey - "GREY_800": "#17181B", # Very Dark Grey - "GREY_900": "#0E1013", # Almost Black - "BLACK": "#000000", # Pure Black + "GREY_100": "#F8F9FA", # Almost White + "GREY_200": "#F1F3F4", # Very Light Grey + "GREY_300": "#DBDDE1", # Light Grey + "GREY_400": "#BDC1C6", # Medium Light Grey + "GREY_500": "#5F6368", # Medium Grey + "GREY_600": "#2E3134", # Medium Dark Grey + "GREY_700": "#282A2D", # Dark Grey + "GREY_800": "#17181B", # Very Dark Grey + "GREY_900": "#0E1013", # Almost Black + "BLACK": "#000000", # Pure Black }, "SUDO": { "HYPERPARAMETER": "#4F91C6", # Medium Blue - "VALUE": "#D09FE9", # Light Purple - "NORMALIZED": "#AB7CC8", # Medium Purple + "VALUE": "#D09FE9", # Light Purple + "NORMALIZED": "#AB7CC8", # Medium Purple }, } From efd1c6b90e3b24c5b80d6ccc6a772dc0b48acd17 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 11:13:18 -0800 Subject: [PATCH 208/332] Skip help table --- bittensor_cli/src/commands/subnets.py | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index e0ef2b94..1a151bb0 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -1296,22 +1296,22 @@ async def show_subnet(netuid_: int): # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) - console.print( - """ - Description: - The table displays the subnet participants and their metrics. - The columns are as follows: - - UID: The hotkey index in the subnet. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. - - Stake: The stake balance of this hotkey on this subnet. - - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. - - Dividends: Validating dividends earned by the hotkey. - - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) - - Emission: The emission accrued to this hokey on this subnet every block (in staking units). - - Hotkey: The hotkey ss58 address. - - Coldkey: The coldkey ss58 address. - """ - ) + # console.print( + # """ + # Description: + # The table displays the subnet participants and their metrics. + # The columns are as follows: + # - UID: The hotkey index in the subnet. + # - TAO: The sum of all TAO balances for this hotkey accross all subnets. + # - Stake: The stake balance of this hotkey on this subnet. + # - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. + # - Dividends: Validating dividends earned by the hotkey. + # - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) + # - Emission: The emission accrued to this hokey on this subnet every block (in staking units). + # - Hotkey: The hotkey ss58 address. + # - Coldkey: The coldkey ss58 address. + # """ + # ) if delegate_selection: while True: From ad4e4f41a8a9be931e20306e8bf025b75c521fc5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 13:40:01 -0800 Subject: [PATCH 209/332] Fixes var --- bittensor_cli/src/commands/stake/stake.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2c43a527..2072c4c1 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1185,7 +1185,7 @@ async def unstake_selection( if not stake_infos: print_error("You have no stakes to unstake.") - return + raise typer.Exit() hotkey_stakes = {} for stake_info in stake_infos: @@ -1593,6 +1593,7 @@ async def unstake( if unstake_all or unstake_all_alpha: return await _unstake_all(wallet, subtensor, unstake_all_alpha, prompt) + unstake_all_from_hk = False with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", From 119918385d1d0814a2055e398a84bd2d4e7f1ff2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 13:41:50 -0800 Subject: [PATCH 210/332] Bumps version and changelog --- CHANGELOG.md | 5 +++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6488bc82..f0bf3f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc7 /2025-01-09 + +## What's Changed +* Returns early in-case of no stake & fixes var + ## 8.2.0rc6 /2025-01-09 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index efcab110..bee82b67 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc6" +__version__ = "7" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7488a4f5..4ae47bc9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc6" +__version__ = "8.2.0rc7" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From cf586383df8d4481d2ed5c1fad5960487b8835f4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 13:43:33 -0800 Subject: [PATCH 211/332] Bumps version --- bittensor_cli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index bee82b67..440edb05 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "7" +__version__ = "8.2.0rc7" __all__ = ["CLIManager", "__version__"] From aa52a6b6933fc20bf70a171d95010d83fc04d724 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 10 Jan 2025 00:27:17 +0200 Subject: [PATCH 212/332] Fixes identity --- bittensor_cli/src/commands/stake/stake.py | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2072c4c1..660c1fb3 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1206,14 +1206,14 @@ async def unstake_selection( hotkeys_info = [] for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): - identity = identities["hotkeys"].get(hotkey_ss58) or old_identities.get( - hotkey_ss58 - ) - hotkey_name = "~" - if identity: - hotkey_name = identity.get("identity", {}).get("name", "") or identity.get( - "display", "~" - ) + if hk_identity := identities["hotkeys"].get(hotkey_ss58): + hotkey_name = hk_identity.get("identity", {}).get( + "name", "" + ) or hk_identity.get("display", "~") + elif old_identity := old_identities.get(hotkey_ss58): + hotkey_name = old_identity.display + else: + hotkey_name = "~" # TODO: Add wallet ids here. hotkeys_info.append( @@ -1479,15 +1479,16 @@ async def _unstake_all( total_received_value += received_amount # Get hotkey identity - identity = ck_hk_identities["hotkeys"].get( - stake.hotkey_ss58 - ) or old_identities.get(stake.hotkey_ss58) - hotkey_display = stake.hotkey_ss58 - if identity: - hotkey_name = identity.get("identity", {}).get( + if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58): + hotkey_name = hk_identity.get("identity", {}).get( "name", "" - ) or identity.get("display", "~") + ) or hk_identity.get("display", "~") hotkey_display = f"{hotkey_name}" + elif old_identity := old_identities.get(stake.hotkey_ss58): + hotkey_name = old_identity.display + hotkey_display = f"{hotkey_name}" + else: + hotkey_display = stake.hotkey_ss58 if dynamic_info.is_dynamic: slippage_pct_float = ( From 2c978a8f0f8daa37948ba579cb946a8c2d22a951 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 14:32:07 -0800 Subject: [PATCH 213/332] Bumps version and changelog --- CHANGELOG.md | 5 +++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bf3f31..c56d1132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc8 /2025-01-09 + +## What's Changed +* Updates identity parsing + ## 8.2.0rc7 /2025-01-09 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 440edb05..07d481e7 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc7" +__version__ = "8.2.0rc8" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4ae47bc9..273aad51 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc7" +__version__ = "8.2.0rc8" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 35ce549b7ecd8b594d78f254a48b51547adfd39d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 15:14:13 -0800 Subject: [PATCH 214/332] Fixes s show/ and delegates --- bittensor_cli/src/commands/subnets.py | 22 ++++++++++++++-------- bittensor_cli/src/commands/sudo.py | 6 +++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 1a151bb0..01e226da 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -801,7 +801,10 @@ async def show( ) -> Optional[str]: async def show_root(): all_subnets = await subtensor.get_all_subnet_dynamic_info() - root_info = all_subnets[0] + root_info = next((s for s in all_subnets if s.netuid == 0), None) + if root_info is None: + print_error("The root subnet does not exist") + raise typer.Exit() hex_bytes_result, identities, old_identities = await asyncio.gather( subtensor.query_runtime_api( @@ -1035,24 +1038,27 @@ async def show_subnet(netuid_: int): subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - subnet_info = _subnet_info[netuid_] + subnet_info = next((s for s in _subnet_info if s.netuid == netuid_), None) + if subnet_info is None: + print_error(f"Subnet {netuid_} does not exist") + raise typer.Exit() if (bytes_result := hex_bytes_result) is None: - err_console.print(f"Subnet {netuid_} does not exist") - return + print_error(f"Subnet {netuid_} does not exist") + raise typer.Exit() if bytes_result.startswith("0x"): bytes_result = bytes.fromhex(bytes_result[2:]) subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) if subnet_info is None: - err_console.print(f"Subnet {netuid_} does not exist") - return + print_error(f"Subnet {netuid_} does not exist") + raise typer.Exit() elif len(subnet_state.hotkeys) == 0: - err_console.print( + print_error( f"Subnet {netuid_} is currently empty with 0 UIDs registered." ) - return + raise typer.Exit() # Define table properties table = Table( diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e85321fe..4703c6f7 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,6 +1,7 @@ import asyncio from typing import TYPE_CHECKING, Union, Optional +import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich import box @@ -495,7 +496,10 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): return False subnet = await subtensor.get_subnet_hyperparameters(netuid) _subnet_info = await subtensor.get_all_subnet_dynamic_info() - subnet_info = _subnet_info[netuid] + subnet_info = next((s for s in _subnet_info if s.netuid == netuid), None) + if subnet_info is None: + print_error(f"Subnet with netuid {netuid} does not exist.") + raise typer.Exit() table = Table( Column("[white]HYPERPARAMETER", style=COLOR_PALETTE['SUDO']['HYPERPARAMETER']), From af60cb5c21bb73825432f0ac55a7e5d0eb5e4dbc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 9 Jan 2025 15:18:41 -0800 Subject: [PATCH 215/332] Bumps version and updates changelog --- CHANGELOG.md | 5 +++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56d1132..e05d972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc9 /2025-01-09 + +## What's Changed +* Updates delegate selection + ## 8.2.0rc8 /2025-01-09 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 07d481e7..57e97493 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc8" +__version__ = "8.2.0rc9" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 273aad51..41f4cf49 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc8" +__version__ = "8.2.0rc9" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From ac375ba2564a65b77b8c1ec8dfda36022e096187 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 10 Jan 2025 12:07:44 -0800 Subject: [PATCH 216/332] Adds block has in dynamic + s list live edge case --- bittensor_cli/src/bittensor/subtensor_interface.py | 3 ++- bittensor_cli/src/commands/subnets.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 03c7f7ed..52f4f7b5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1392,10 +1392,11 @@ async def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore - async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: + async def get_all_subnet_dynamic_info(self, block_hash: Optional[str] = None) -> list["DynamicInfo"]: query = await self.substrate.runtime_call( "SubnetInfoRuntimeApi", "get_all_dynamic_info", + block_hash=block_hash, ) subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) return subnets diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 01e226da..8e687f15 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -534,7 +534,7 @@ def format_liquidity_cell( "supply": supply, "blocks_since_last_step": subnet.blocks_since_last_step, } - prev = previous_data.get(netuid) if previous_data else {} + prev = previous_data.get(netuid, {}) if previous_data else {} # Prepare cells if netuid == 0: From c836528626f073d5fc1540406e72ec458de28046 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sat, 11 Jan 2025 09:57:17 -0800 Subject: [PATCH 217/332] Fixes bug in stake list + bumps version and changelog --- CHANGELOG.md | 5 +++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 8 +++----- bittensor_cli/src/commands/subnets.py | 6 ++---- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05d972d..8665d313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc10 /2025-01-11 + +## What's Changed +* Fixes data ordering in stake list + ## 8.2.0rc9 /2025-01-09 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 57e97493..d0780395 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc9" +__version__ = "8.2.0rc10" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 41f4cf49..67f61bc2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc9" +__version__ = "8.2.0rc10" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 660c1fb3..521b1aa2 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1999,7 +1999,7 @@ async def get_stake_data(block_hash: str = None): ( substakes, registered_delegate_info, - dynamic_info, + _dynamic_info, ) = await asyncio.gather( subtensor.get_stake_info_for_coldkeys( coldkey_ss58_list=[coldkey_address], block_hash=block_hash @@ -2008,6 +2008,7 @@ async def get_stake_data(block_hash: str = None): subtensor.get_all_subnet_dynamic_info(), ) sub_stakes = substakes[coldkey_address] + dynamic_info = {info.netuid: info for info in _dynamic_info} return ( sub_stakes, registered_delegate_info, @@ -2195,9 +2196,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): f"{stake_value} {symbol}" if netuid != 0 else f"{symbol} {stake_value}", # Stake (a) - f"{millify_tao(pool.price.tao)} τ/{symbol}" - if not verbose - else f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) + f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) # f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv swap_value, # Swap(α) -> τ "YES" @@ -2483,7 +2482,6 @@ def format_cell( if stake.hotkey_ss58 == selected_hotkey ] - dynamic_info = {info.netuid: info for info in dynamic_info_} block_number = await subtensor.substrate.get_block_number(None) previous_block = current_block diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 8e687f15..dc818664 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -324,9 +324,7 @@ def create_table(subnets, block_number): else f"{subnet.alpha_out.tao:,.4f}" ) price_value = ( - f"{millify_tao(subnet.price.tao)}" - if not verbose - else f"{subnet.price.tao:,.4f}" + f"{subnet.price.tao:,.4f}" ) # Market Cap @@ -559,7 +557,7 @@ def format_liquidity_cell( prev.get("price"), unit=f"τ/{symbol}", precision=4, - millify=True if not verbose else False, + millify=False, ) alpha_out_cell = format_cell( From 14f38718c33a4f93e7b3447042c20de2acd7847e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 14 Jan 2025 15:55:35 -0800 Subject: [PATCH 218/332] Updates btcli w overview command --- bittensor_cli/cli.py | 18 +- bittensor_cli/src/bittensor/chain_data.py | 16 +- bittensor_cli/src/commands/stake/stake.py | 4 + bittensor_cli/src/commands/subnets.py | 51 +++-- bittensor_cli/src/commands/wallets.py | 222 ++++++---------------- 5 files changed, 107 insertions(+), 204 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 67f61bc2..390962a3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -748,7 +748,7 @@ def __init__(self): )(self.subnets_register) self.subnets_app.command( "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True - )(self.subnets_show) #Aliased to `s show` for now + )(self.subnets_show) # Aliased to `s show` for now self.subnets_app.command( "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) @@ -1409,12 +1409,6 @@ def wallet_overview( "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) - # For Rao games - effective_network = get_effective_network(self.config, network) - if is_rao_network(effective_network): - print_error("This command is disabled on the 'rao' network.") - raise typer.Exit() - return self._run_command( wallets.overview( wallet, @@ -1425,6 +1419,7 @@ def wallet_overview( include_hotkeys, exclude_hotkeys, netuids_filter=netuids, + verbose=verbose, ) ) @@ -1600,6 +1595,8 @@ def wallet_inspect( [bold]Note[/bold]: The `inspect` command is for displaying information only and does not perform any transactions or state changes on the blockchain. It is intended to be used with Bittensor CLI and not as a standalone function in user code. """ + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() self.verbosity_handler(quiet, verbose) if netuids: @@ -1615,11 +1612,6 @@ def wallet_inspect( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate ) - # For Rao games - effective_network = get_effective_network(self.config, network) - if is_rao_network(effective_network): - print_error("This command is disabled on the 'rao' network.") - raise typer.Exit() self.initialize_chain(network) return self._run_command( @@ -2791,7 +2783,7 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) - # TODO: Coldkey related unstakes need to be updated. Patching for now. + # TODO: Coldkey related unstakes need to be updated. Patching for now. unstake_all_alpha = False unstake_all = False diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index f5596f1a..aae2ea34 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -97,11 +97,13 @@ def decode_hex_identity(info_dictionary): return decoded_info -def process_stake_data(stake_data): +def process_stake_data(stake_data, netuid): decoded_stake_data = {} for account_id_bytes, stake_ in stake_data: account_id = decode_account_id(account_id_bytes) - decoded_stake_data.update({account_id: Balance.from_rao(stake_)}) + decoded_stake_data.update( + {account_id: Balance.from_rao(stake_).set_unit(netuid)} + ) return decoded_stake_data @@ -371,7 +373,7 @@ def get_null_neuron() -> "NeuronInfo": @classmethod def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": n = bt_decode.NeuronInfo.decode(vec_u8) - stake_dict = process_stake_data(n.stake) + stake_dict = process_stake_data(n.stake, n.netuid) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) axon_info = n.axon_info coldkey = decode_account_id(n.coldkey) @@ -491,8 +493,12 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfoLite"]: prometheus_info = item.prometheus_info pruning_score = item.pruning_score rank = item.rank - stake_dict = process_stake_data(item.stake) - stake = sum(stake_dict.values()) if stake_dict else Balance(0) + stake_dict = process_stake_data(item.stake, item.netuid) + stake = ( + sum(stake_dict.values()) + if stake_dict + else Balance(0).set_unit(item.netuid) + ) trust = item.trust uid = item.uid validator_permit = item.validator_permit diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 521b1aa2..f603f751 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -2424,6 +2424,10 @@ def format_cell( hotkeys_to_substakes[hotkey] = [] hotkeys_to_substakes[hotkey].append(substake) + if not hotkeys_to_substakes: + print_error(f"No stakes found for coldkey ss58: ({coldkey_address})") + raise typer.Exit() + if live: # Select one hokkey for live monitoring if len(hotkeys_to_substakes) > 1: diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index dc818664..dc1e705d 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -228,7 +228,9 @@ def calculate_emission_stats( if not verbose: percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number)} ({formatted_percentage})" else: - percentage_string = f"τ {total_tao_emitted:.1f}/{block_number} ({formatted_percentage})" + percentage_string = ( + f"τ {total_tao_emitted:.1f}/{block_number} ({formatted_percentage})" + ) return total_tao_emitted, percentage_string def define_table( @@ -323,9 +325,7 @@ def create_table(subnets, block_number): if not verbose else f"{subnet.alpha_out.tao:,.4f}" ) - price_value = ( - f"{subnet.price.tao:,.4f}" - ) + price_value = f"{subnet.price.tao:,.4f}" # Market Cap market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao @@ -343,8 +343,8 @@ def create_table(subnets, block_number): if netuid != 0 else "-" ) - alpha_in_cell = f"{alpha_in_value} {symbol}" if netuid != 0 else "-" + liquidity_cell = f"{tao_in_cell}, {alpha_in_cell}" # Supply supply = subnet.alpha_in.tao + subnet.alpha_out.tao @@ -358,7 +358,6 @@ def create_table(subnets, block_number): ) emission_cell = f"τ {emission_tao:,.4f}" price_cell = f"{price_value} τ/{symbol}" - liquidity_cell = f"{tao_in_cell}, {alpha_in_cell}" alpha_out_cell = ( f"{alpha_out_value} {symbol}" if netuid != 0 @@ -1053,9 +1052,7 @@ async def show_subnet(netuid_: int): print_error(f"Subnet {netuid_} does not exist") raise typer.Exit() elif len(subnet_state.hotkeys) == 0: - print_error( - f"Subnet {netuid_} is currently empty with 0 UIDs registered." - ) + print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.") raise typer.Exit() # Define table properties @@ -1174,7 +1171,9 @@ async def show_subnet(netuid_: int): f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake - f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake + f"τ {tao_stake.tao:.4f}" + if verbose + else f"τ {millify_tao(tao_stake)}", # Tao Stake # str(subnet_state.dividends[idx]), f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends f"{subnet_state.incentives[idx]:.4f}", # Incentive @@ -1300,22 +1299,22 @@ async def show_subnet(netuid_: int): # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) - # console.print( - # """ - # Description: - # The table displays the subnet participants and their metrics. - # The columns are as follows: - # - UID: The hotkey index in the subnet. - # - TAO: The sum of all TAO balances for this hotkey accross all subnets. - # - Stake: The stake balance of this hotkey on this subnet. - # - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. - # - Dividends: Validating dividends earned by the hotkey. - # - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) - # - Emission: The emission accrued to this hokey on this subnet every block (in staking units). - # - Hotkey: The hotkey ss58 address. - # - Coldkey: The coldkey ss58 address. - # """ - # ) + # console.print( + # """ + # Description: + # The table displays the subnet participants and their metrics. + # The columns are as follows: + # - UID: The hotkey index in the subnet. + # - TAO: The sum of all TAO balances for this hotkey accross all subnets. + # - Stake: The stake balance of this hotkey on this subnet. + # - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. + # - Dividends: Validating dividends earned by the hotkey. + # - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) + # - Emission: The emission accrued to this hokey on this subnet every block (in staking units). + # - Hotkey: The hotkey ss58 address. + # - Coldkey: The coldkey ss58 address. + # """ + # ) if delegate_selection: while True: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 34e5399a..5d2d9a91 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -56,9 +56,18 @@ is_valid_ss58_address, validate_coldkey_presence, retry_prompt, + get_subnet_name, + millify_tao, ) +class WalletLike: + def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None): + self.name = name + self.hotkey_ss58 = hotkey_ss58 + self.hotkey_str = hotkey_str + + async def regen_coldkey( wallet: Wallet, mnemonic: Optional[str], @@ -603,17 +612,19 @@ async def overview( include_hotkeys: Optional[list[str]] = None, exclude_hotkeys: Optional[list[str]] = None, netuids_filter: Optional[list[int]] = None, + verbose: bool = False, ): """Prints an overview for the wallet's coldkey.""" total_balance = Balance(0) # We are printing for every coldkey. - print_verbose("Fetching total balance for coldkey/s") block_hash = await subtensor.substrate.get_chain_head() all_hotkeys, total_balance = await _get_total_balance( total_balance, subtensor, wallet, all_wallets, block_hash=block_hash ) + _dynamic_info = await subtensor.get_all_subnet_dynamic_info() + dynamic_info = {info.netuid: info for info in _dynamic_info} with console.status( f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]", @@ -621,9 +632,6 @@ async def overview( ) as status: # We are printing for a select number of hotkeys from all_hotkeys. if include_hotkeys or exclude_hotkeys: - print_verbose( - "Fetching for select hotkeys passed in 'include_hotkeys'", status - ) all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys) # Check we have keys to display. @@ -633,17 +641,14 @@ async def overview( # Pull neuron info for all keys. neurons: dict[str, list[NeuronInfoLite]] = {} - print_verbose("Fetching subnet netuids", status) block, all_netuids = await asyncio.gather( subtensor.substrate.get_block_number(None), subtensor.get_all_subnet_netuids(), ) - print_verbose("Filtering netuids by registered hotkeys", status) netuids = await subtensor.filter_netuids_by_registered_hotkeys( all_netuids, netuids_filter, all_hotkeys, reuse_block=True ) - # bittensor.logging.debug(f"Netuids to check: {netuids}") for netuid in netuids: neurons[str(netuid)] = [] @@ -664,104 +669,17 @@ async def overview( ) all_hotkeys, _ = validate_coldkey_presence(all_hotkeys) - print_verbose("Fetching key addresses", status) all_hotkey_addresses, hotkey_coldkey_to_hotkey_wallet = _get_key_address( all_hotkeys ) - print_verbose("Pulling and processing neuron information for all keys", status) results = await _get_neurons_for_netuids( subtensor, netuids, all_hotkey_addresses ) neurons = _process_neuron_results(results, neurons, netuids) - total_coldkey_stake_from_metagraph = await _calculate_total_coldkey_stake( - neurons - ) - - alerts_table = Table(show_header=True, header_style="bold magenta") - alerts_table.add_column("🥩 alert!") - - coldkeys_to_check = [] - ck_stakes = await subtensor.get_total_stake_for_coldkey( - *( - coldkey_wallet.coldkeypub.ss58_address - for coldkey_wallet in all_coldkey_wallets - if coldkey_wallet.coldkeypub - ), - block_hash=block_hash, - ) - for coldkey_wallet in all_coldkey_wallets: - if coldkey_wallet.coldkeypub: - # Check if we have any stake with hotkeys that are not registered. - difference = ( - ck_stakes[coldkey_wallet.coldkeypub.ss58_address] - - total_coldkey_stake_from_metagraph[ - coldkey_wallet.coldkeypub.ss58_address - ] - ) - if difference == 0: - continue # We have all our stake registered. - - coldkeys_to_check.append(coldkey_wallet) - alerts_table.add_row( - "Found [light_goldenrod2]{}[/light_goldenrod2] stake with coldkey [bright_magenta]{}[/bright_magenta] that is not registered.".format( - abs(difference), coldkey_wallet.coldkeypub.ss58_address - ) - ) - - if coldkeys_to_check: - # We have some stake that is not with a registered hotkey. - if "-1" not in neurons: - neurons["-1"] = [] - - print_verbose("Checking coldkeys for de-registered stake", status) - results = await asyncio.gather( - *[ - _get_de_registered_stake_for_coldkey_wallet( - subtensor, all_hotkey_addresses, coldkey_wallet - ) - for coldkey_wallet in coldkeys_to_check - ] - ) - - for result in results: - coldkey_wallet, de_registered_stake, err_msg = result - if err_msg is not None: - err_console.print(err_msg) - - if len(de_registered_stake) == 0: - continue # We have no de-registered stake with this coldkey. - - de_registered_neurons = [] - for hotkey_addr, our_stake in de_registered_stake: - # Make a neuron info lite for this hotkey and coldkey. - de_registered_neuron = NeuronInfoLite.get_null_neuron() - de_registered_neuron.hotkey = hotkey_addr - de_registered_neuron.coldkey = coldkey_wallet.coldkeypub.ss58_address - de_registered_neuron.total_stake = Balance(our_stake) - de_registered_neurons.append(de_registered_neuron) - - # Add this hotkey to the wallets dict - wallet_ = Wallet(name=wallet) - wallet_.hotkey_ss58 = hotkey_addr - wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters - # Indicates a hotkey not on local machine but exists in stake_info obj on-chain - if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: - hotkey_coldkey_to_hotkey_wallet[hotkey_addr] = {} - hotkey_coldkey_to_hotkey_wallet[hotkey_addr][ - coldkey_wallet.coldkeypub.ss58_address - ] = wallet_ - - # Add neurons to overview. - neurons["-1"].extend(de_registered_neurons) - # Setup outer table. grid = Table.grid(pad_edge=True) - # If there are any alerts, add them to the grid - if len(alerts_table.rows) > 0: - grid.add_row(alerts_table) - # Add title if not all_wallets: title = "[underline dark_orange]Wallet[/underline dark_orange]\n" @@ -780,9 +698,6 @@ async def overview( ) ) # Generate rows per netuid - hotkeys_seen = set() - total_neurons = 0 - total_stake = 0.0 tempos = await asyncio.gather( *[ subtensor.get_hyperparameter("Tempo", netuid, block_hash) @@ -790,7 +705,6 @@ async def overview( ] ) for netuid, subnet_tempo in zip(netuids, tempos): - last_subnet = netuid == netuids[-1] table_data = [] total_rank = 0.0 total_trust = 0.0 @@ -799,6 +713,8 @@ async def overview( total_incentive = 0.0 total_dividends = 0.0 total_emission = 0 + total_stake = 0 + total_neurons = 0 for nn in neurons[str(netuid)]: hotwallet = hotkey_coldkey_to_hotkey_wallet.get(nn.hotkey, {}).get( @@ -807,8 +723,7 @@ async def overview( if not hotwallet: # Indicates a mismatch between what the chain says the coldkey # is for this hotkey and the local wallet coldkey-hotkey pair - hotwallet = Wallet(name=nn.coldkey[:7]) - hotwallet.hotkey_str = nn.hotkey[:7] + hotwallet = WalletLike(name=nn.coldkey[:7], hotkey_str=nn.hotkey[:7]) nn: NeuronInfoLite uid = nn.uid @@ -820,7 +735,7 @@ async def overview( validator_trust = nn.validator_trust incentive = nn.incentive dividends = nn.dividends - emission = int(nn.emission / (subnet_tempo + 1) * 1e9) + emission = int(nn.emission / (subnet_tempo + 1) * 1e9) # Per block last_update = int(block - nn.last_update) validator_permit = nn.validator_permit row = [ @@ -828,14 +743,14 @@ async def overview( hotwallet.hotkey_str, str(uid), str(active), - "{:.5f}".format(stake), - "{:.5f}".format(rank), - "{:.5f}".format(trust), - "{:.5f}".format(consensus), - "{:.5f}".format(incentive), - "{:.5f}".format(dividends), - "{:_}".format(emission), - "{:.5f}".format(validator_trust), + f"{stake:.4f}" if verbose else millify_tao(stake), + f"{rank:.4f}" if verbose else millify_tao(rank), + f"{trust:.4f}" if verbose else millify_tao(trust), + f"{consensus:.4f}" if verbose else millify_tao(consensus), + f"{incentive:.4f}" if verbose else millify_tao(incentive), + f"{dividends:.4f}" if verbose else millify_tao(dividends), + f"{emission:.4f}", + f"{validator_trust:.4f}" if verbose else millify_tao(validator_trust), "*" if validator_permit else "", str(last_update), ( @@ -853,23 +768,15 @@ async def overview( total_dividends += dividends total_emission += emission total_validator_trust += validator_trust - - if (nn.hotkey, nn.coldkey) not in hotkeys_seen: - # Don't double count stake on hotkey-coldkey pairs. - hotkeys_seen.add((nn.hotkey, nn.coldkey)) - total_stake += stake - - # netuid -1 are neurons that are de-registered. - if netuid != "-1": - total_neurons += 1 + total_stake += stake + total_neurons += 1 table_data.append(row) # Add subnet header - if netuid == "-1": - grid.add_row("Deregistered Neurons") - else: - grid.add_row(f"Subnet: [dark_orange]{netuid}[/dark_orange]") + grid.add_row( + f"Subnet: [dark_orange]{netuid}: {get_subnet_name(dynamic_info[netuid])} {dynamic_info[netuid].symbol}[/dark_orange]" + ) width = console.width table = Table( show_footer=False, @@ -878,45 +785,34 @@ async def overview( expand=True, width=width - 5, ) - if last_subnet: - table.add_column( - "[white]COLDKEY", str(total_neurons), style="bold bright_cyan", ratio=2 - ) - table.add_column( - "[white]HOTKEY", str(total_neurons), style="bright_cyan", ratio=2 - ) - else: - # No footer for non-last subnet. - table.add_column("[white]COLDKEY", style="bold bright_cyan", ratio=2) - table.add_column("[white]HOTKEY", style="bright_cyan", ratio=2) + + table.add_column("[white]COLDKEY", style="bold bright_cyan", ratio=2) + table.add_column("[white]HOTKEY", style="bright_cyan", ratio=2) table.add_column( "[white]UID", str(total_neurons), style="rgb(42,161,152)", ratio=1 ) table.add_column( "[white]ACTIVE", justify="right", style="#8787ff", no_wrap=True, ratio=1 ) - if last_subnet: - table.add_column( - "[white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="bold white", - justify="right", - style="dark_orange", - no_wrap=True, - ratio=1, - ) - else: - # No footer for non-last subnet. - table.add_column( - "[white]STAKE(\u03c4)", - justify="right", - style="dark_orange", - no_wrap=True, - ratio=1.5, - ) + + _total_stake_formatted = ( + f"{total_stake:.4f}" if verbose else millify_tao(total_stake) + ) + table.add_column( + "[white]STAKE(\u03c4)" + if netuid == 0 + else f"[white]STAKE({Balance.get_unit(netuid)})", + f"{_total_stake_formatted} {Balance.get_unit(netuid)}" + if netuid != 0 + else f"{Balance.get_unit(netuid)} {_total_stake_formatted}", + justify="right", + style="dark_orange", + no_wrap=True, + ratio=1.5, + ) table.add_column( "[white]RANK", - "{:.5f}".format(total_rank), + f"{total_rank:.4f}", justify="right", style="medium_purple", no_wrap=True, @@ -924,7 +820,7 @@ async def overview( ) table.add_column( "[white]TRUST", - "{:.5f}".format(total_trust), + f"{total_trust:.4f}", justify="right", style="green", no_wrap=True, @@ -932,7 +828,7 @@ async def overview( ) table.add_column( "[white]CONSENSUS", - "{:.5f}".format(total_consensus), + f"{total_consensus:.4f}", justify="right", style="rgb(42,161,152)", no_wrap=True, @@ -940,7 +836,7 @@ async def overview( ) table.add_column( "[white]INCENTIVE", - "{:.5f}".format(total_incentive), + f"{total_incentive:.4f}", justify="right", style="#5fd7ff", no_wrap=True, @@ -948,7 +844,7 @@ async def overview( ) table.add_column( "[white]DIVIDENDS", - "{:.5f}".format(total_dividends), + f"{total_dividends:.4f}", justify="right", style="#8787d7", no_wrap=True, @@ -956,7 +852,7 @@ async def overview( ) table.add_column( "[white]EMISSION(\u03c1)", - "\u03c1{:_}".format(total_emission), + f"\u03c1{total_emission}", justify="right", style="#d7d7ff", no_wrap=True, @@ -964,7 +860,7 @@ async def overview( ) table.add_column( "[white]VTRUST", - "{:.5f}".format(total_validator_trust), + f"{total_validator_trust:.4f}", justify="right", style="magenta", no_wrap=True, @@ -1283,6 +1179,8 @@ def delegate_row_maker( delegates_: list[tuple[DelegateInfo, Balance]], ) -> Generator[list[str], None, None]: for d_, staked in delegates_: + if not staked.tao > 0: + continue if d_.hotkey_ss58 in registered_delegate_info: delegate_name = registered_delegate_info[d_.hotkey_ss58].display else: @@ -1292,7 +1190,11 @@ def delegate_row_maker( + [ str(delegate_name), str(staked), - str(d_.total_daily_return.tao * (staked.tao / d_.total_stake.tao)), + str( + d_.total_daily_return.tao * (staked.tao / d_.total_stake.tao) + if d_.total_stake.tao != 0 + else 0 + ), ] + [""] * 4 ) From 5da64b6338fc5a17eb825341ab6aa567e6bf6ac7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 14 Jan 2025 19:29:18 -0800 Subject: [PATCH 219/332] Adds price cmd --- CHANGELOG.md | 6 ++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 32 +++++- bittensor_cli/src/bittensor/chain_data.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 4 +- bittensor_cli/src/commands/subnets.py | 123 ++++++++++++++++++++-- bittensor_cli/src/commands/sudo.py | 20 ++-- bittensor_cli/src/commands/wallets.py | 15 +-- 8 files changed, 170 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8665d313..8714fff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 8.2.0rc11 /2025-01-14 + +## What's Changed +* Adds subnet price command +* Ports w overview command + ## 8.2.0rc10 /2025-01-11 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index d0780395..dbdf9ca1 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc10" +__version__ = "8.2.0rc11" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 390962a3..b7e4f1c2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc10" +__version__ = "8.2.0rc11" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -645,12 +645,13 @@ def __init__(self): "balance", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] )(self.wallet_balance) self.wallet_app.command( - "history", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] + "history", + rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], + hidden=True, )(self.wallet_history) self.wallet_app.command( "overview", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], - hidden=True, )(self.wallet_overview) self.wallet_app.command( "transfer", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] @@ -752,6 +753,9 @@ def __init__(self): self.subnets_app.command( "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) + self.subnets_app.command( + "price", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True + )(self.subnets_price) # weights commands self.weights_app.command( @@ -3554,6 +3558,28 @@ def subnets_list( ) ) + def subnets_price( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + interval_hours: int = typer.Option( + 24, + "--interval-hours", + "--interval", + help="The number of hours to show the historical price for.", + ), + ): + """ + Shows the historical price of a subnet for the past 24 hours. + + EXAMPLE + + [green]$[/green] btcli subnets price --netuid 1 + """ + return self._run_command( + subnets.price(self.initialize_chain(network), netuid, interval_hours) + ) + def subnets_show( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index aae2ea34..0aebe3ca 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -905,7 +905,7 @@ class DynamicInfo: def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: if len(vec_u8) == 0: return None - decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo, is_option=True) + decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo) if decoded is None: return None return DynamicInfo.fix_decoded_values(decoded) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index f603f751..d27719b2 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -10,11 +10,11 @@ from rich.table import Table from rich import box from rich.progress import Progress, BarColumn, TextColumn -from rich.console import Console, Group +from rich.console import Group from rich.live import Live from substrateinterface.exceptions import SubstrateRequestException -from bittensor_cli.src import COLOR_PALETTE, SUBNETS +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index dc1e705d..b3294538 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -3,18 +3,17 @@ import sqlite3 from typing import TYPE_CHECKING, Optional, cast import typer +import plotille from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, Prompt -from rich.console import Console, Group -from rich.spinner import Spinner -from rich.text import Text +from rich.console import Group from rich.progress import Progress, BarColumn, TextColumn from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE, SUBNETS +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( @@ -26,7 +25,6 @@ from bittensor_cli.src.bittensor.minigraph import MiniGraph from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( - RAO_PER_TAO, console, create_table, err_console, @@ -1153,7 +1151,7 @@ async def show_subnet(netuid_: int): ): if uid_identity == "~": uid_identity = ( - f"[dark_sea_green3](*Owner controlled)[/dark_sea_green3]" + "[dark_sea_green3](*Owner controlled)[/dark_sea_green3]" ) else: uid_identity = ( @@ -2053,3 +2051,116 @@ async def metagraph_cmd( table.add_row(*row) console.print(table) + + +async def price( + subtensor: "SubtensorInterface", + netuid: int, + interval_hours: int = 24, +): + """Plot historical subnet price data in the CLI.""" + + blocks_per_hour = int(3600 / 12) # ~300 blocks per hour + total_blocks = blocks_per_hour * interval_hours + + with console.status(":chart_increasing: Fetching historical price data..."): + current_block_hash = await subtensor.substrate.get_chain_head() + current_block = await subtensor.substrate.get_block_number(current_block_hash) + + # Block range + step = 300 + start_block = max(0, current_block - total_blocks) + block_numbers = range(start_block, current_block + 1, step) + + # Fetch all block hashes + block_hash_cors = [ + subtensor.substrate.get_block_hash(bn) for bn in block_numbers + ] + block_hashes = await asyncio.gather(*block_hash_cors) + + # Fetch subnet data for each block + subnet_info_cors = [ + subtensor.get_subnet_dynamic_info(netuid, bh) for bh in block_hashes + ] + subnet_infos = await asyncio.gather(*subnet_info_cors) + + prices = [] + for subnet_info in subnet_infos: + prices.append(subnet_info.price.tao) + + if not prices: + err_console.print(f"[red]No price data found for subnet {netuid}[/red]") + return + + fig = plotille.Figure() + fig.width = 60 + fig.height = 20 + fig.color_mode = "rgb" + fig.background = None + + block_numbers = list(range(current_block - total_blocks, current_block + 1, step)) + + def color_label(text): + return plotille.color(text, fg=(186, 233, 143), mode="rgb") # Green + + fig.x_label = color_label("Block") + fig.y_label = color_label(f"Price ({Balance.get_unit(netuid)})") + + fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers)) + fig.set_y_limits(min_=min(prices) * 0.95, max_=max(prices) * 1.05) + + fig.plot( + block_numbers, + prices, + label=f"Subnet {netuid} Price", + interp="linear", + lc="bae98f", # Green hex + ) + + subnet = subnet_infos[-1] + console.print( + f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {subnet.symbol} [cyan]{get_subnet_name(subnet)}" + ) + console.print( + f"Current: [blue]{prices[-1]:.6f}{Balance.get_unit(netuid)}" + if netuid != 0 + else f"Current: [blue]{Balance.get_unit(netuid)} {prices[-1]:.6f}" + ) + console.print( + f"{interval_hours}h High: [dark_sea_green3]{max(prices):.6f}{Balance.get_unit(netuid)}" + if netuid != 0 + else f"{interval_hours}h High: [dark_sea_green3]{Balance.get_unit(netuid)} {max(prices):.6f}" + ) + console.print( + f"{interval_hours}h Low: [red]{min(prices):.6f}{Balance.get_unit(netuid)}" + if netuid != 0 + else f"{interval_hours}h Low: [red]{Balance.get_unit(netuid)} {min(prices):.6f}" + ) + + change_color = "dark_sea_green3" if prices[-1] > prices[0] else "red" + console.print( + f"{interval_hours}h Change: " + f"[{change_color}]{((prices[-1] - prices[0]) / prices[0] * 100):.2f}%\n" + ) + print(fig.show()) + console.print("\nLatest stats:") + console.print( + f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet.alpha_in.tao + subnet.alpha_out.tao:,.2f} {Balance.get_unit(netuid)}" + if netuid != 0 + else f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{Balance.get_unit(netuid)} {subnet.alpha_in.tao + subnet.alpha_out.tao:,.2f}" + ) + console.print( + f"Market Cap: [steel_blue3]{subnet.price.tao * (subnet.alpha_in.tao + subnet.alpha_out.tao):,.2f} {Balance.get_unit(netuid)} / 21M" + if netuid != 0 + else f"Market Cap: [steel_blue3]{Balance.get_unit(netuid)} {subnet.price.tao * (subnet.alpha_in.tao + subnet.alpha_out.tao):,.2f} / 21M" + ) + console.print( + f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]{subnet.emission.tao:,.2f} {Balance.get_unit(netuid)}" + if netuid != 0 + else f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]{Balance.get_unit(netuid)} {subnet.emission.tao:,.2f}" + ) + console.print( + f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]{subnet.alpha_out.tao:,.2f} {Balance.get_unit(netuid)}" + if netuid != 0 + else f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]{Balance.get_unit(netuid)} {subnet.alpha_out.tao:,.2f}" + ) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4703c6f7..e7eb151d 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -9,7 +9,7 @@ from rich.prompt import Confirm from scalecodec import GenericCall -from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE, SUBNETS +from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -445,7 +445,9 @@ async def set_take_extrinsic( if not success: err_console.print(err) else: - console.print(":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]") + console.print( + ":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]" + ) return success @@ -502,9 +504,9 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): raise typer.Exit() table = Table( - Column("[white]HYPERPARAMETER", style=COLOR_PALETTE['SUDO']['HYPERPARAMETER']), - Column("[white]VALUE", style=COLOR_PALETTE['SUDO']['VALUE']), - Column("[white]NORMALIZED", style=COLOR_PALETTE['SUDO']['NORMALIZED']), + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE["SUDO"]["HYPERPARAMETER"]), + Column("[white]VALUE", style=COLOR_PALETTE["SUDO"]["VALUE"]), + Column("[white]NORMALIZED", style=COLOR_PALETTE["SUDO"]["NORMALIZED"]), title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" @@ -716,10 +718,14 @@ async def _do_set_take() -> bool: return False else: new_take = await get_current_take(subtensor, wallet) - console.print(f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%") + console.print( + f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%" + ) return True - console.print(f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}") + console.print( + f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}" + ) try: wallet.unlock_hotkey() diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 5d2d9a91..81270ee0 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1,12 +1,7 @@ import asyncio -import binascii import itertools import os -import sys from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor -from functools import partial -from sys import getsizeof from typing import Any, Collection, Generator, Optional import aiohttp @@ -16,29 +11,23 @@ from fuzzywuzzy import fuzz from rich import box from rich.align import Align -from rich.prompt import Confirm, Prompt from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding -from rich.prompt import IntPrompt from scalecodec import ScaleBytes -import scalecodec import typer -from bittensor_cli.src import TYPE_REGISTRY, COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, NeuronInfoLite, StakeInfo, - custom_rpc_type_registry, - decode_account_id, ) from bittensor_cli.src.bittensor.extrinsics.registration import ( run_faucet_extrinsic, swap_hotkey_extrinsic, - is_hotkey_registered, ) from bittensor_cli.src.bittensor.extrinsics.transfer import transfer_extrinsic from bittensor_cli.src.bittensor.networking import int_to_ip @@ -47,7 +36,6 @@ RAO_PER_TAO, console, convert_blocks_to_time, - decode_scale_bytes, err_console, print_error, print_verbose, @@ -55,7 +43,6 @@ get_hotkey_wallets_for_wallet, is_valid_ss58_address, validate_coldkey_presence, - retry_prompt, get_subnet_name, millify_tao, ) From 4ca510e14dea0362580d55335a18f05f08b85c72 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 14 Jan 2025 19:38:25 -0800 Subject: [PATCH 220/332] Bumps installation reqs for plotille --- CHANGELOG.md | 5 +++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- requirements.txt | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8714fff8..1a879b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.2.0rc12 /2025-01-14 + +## What's Changed +* Bumps requirements + ## 8.2.0rc11 /2025-01-14 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index dbdf9ca1..7d19952b 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc11" +__version__ = "8.2.0rc12" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b7e4f1c2..fd920eb8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc11" +__version__ = "8.2.0rc12" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/requirements.txt b/requirements.txt index 369fa115..f4f62160 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,5 @@ substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 bittensor-wallet>=2.0.2 -bt-decode==0.4.0 \ No newline at end of file +bt-decode==0.4.0 +plotille \ No newline at end of file From c5adb0186c70ff59cc8c7bfcc9a3432d2c75cb34 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 15 Jan 2025 08:55:29 -0800 Subject: [PATCH 221/332] Handles in-case data isn't passed back --- bittensor_cli/src/commands/subnets.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index b3294538..a8310914 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -2084,12 +2084,22 @@ async def price( ] subnet_infos = await asyncio.gather(*subnet_info_cors) - prices = [] - for subnet_info in subnet_infos: - prices.append(subnet_info.price.tao) + valid_data = [ + (bn, info) + for bn, info in zip(block_numbers, subnet_infos) + if info is not None + ] + if not valid_data: + err_console.print(f"[red]No price data found for subnet {netuid}[/red]") + return + + block_numbers, subnet_infos = zip(*valid_data) + prices = [subnet_info.price.tao for subnet_info in subnet_infos] - if not prices: - err_console.print(f"[red]No price data found for subnet {netuid}[/red]") + if len(prices) < 5: + err_console.print( + f"[red]Insufficient price data for subnet {netuid}. Need at least 5 data points but only found {len(prices)}.[/red]" + ) return fig = plotille.Figure() @@ -2098,8 +2108,6 @@ async def price( fig.color_mode = "rgb" fig.background = None - block_numbers = list(range(current_block - total_blocks, current_block + 1, step)) - def color_label(text): return plotille.color(text, fg=(186, 233, 143), mode="rgb") # Green From 001c3aa21de55b6af19117dbf70dfc86e448f367 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 15 Jan 2025 08:55:57 -0800 Subject: [PATCH 222/332] Bumps version --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 7d19952b..d0da2734 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc12" +__version__ = "8.2.0rc13" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fd920eb8..dda56680 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -62,7 +62,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc12" +__version__ = "8.2.0rc13" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 7b3aad41f3ba753f8c05e3fb2c1cf5f00c454f98 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 21 Jan 2025 16:20:00 -0800 Subject: [PATCH 223/332] Updates price cmd with html --- bittensor_cli/cli.py | 36 +- bittensor_cli/src/commands/subnets/price.py | 834 ++++++++++++++++++ .../src/commands/{ => subnets}/subnets.py | 122 --- requirements.txt | 4 +- 4 files changed, 869 insertions(+), 127 deletions(-) create mode 100644 bittensor_cli/src/commands/subnets/price.py rename bittensor_cli/src/commands/{ => subnets}/subnets.py (94%) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dda56680..93863f5d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -29,8 +29,9 @@ from bittensor_cli.src.bittensor.async_substrate_interface import ( SubstrateRequestException, ) -from bittensor_cli.src.commands import subnets, sudo, wallets +from bittensor_cli.src.commands import sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds +from bittensor_cli.src.commands.subnets import price, subnets from bittensor_cli.src.commands.stake import children_hotkeys, stake from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters @@ -754,7 +755,7 @@ def __init__(self): "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_show) self.subnets_app.command( - "price", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True + "price", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_price) # weights commands @@ -3561,13 +3562,24 @@ def subnets_list( def subnets_price( self, network: Optional[list[str]] = Options.network, - netuid: int = Options.netuid, + netuid: Optional[int] = typer.Option( + None, + "--netuid", + help="The netuid of the subnet (e.g. 1)", + ), interval_hours: int = typer.Option( 24, "--interval-hours", "--interval", help="The number of hours to show the historical price for.", ), + all_netuids: bool = typer.Option( + False, + "--all-netuids", + "--all", + help="Show the price for all subnets.", + ), + html_output: bool = Options.html_output, ): """ Shows the historical price of a subnet for the past 24 hours. @@ -3576,8 +3588,24 @@ def subnets_price( [green]$[/green] btcli subnets price --netuid 1 """ + if all_netuids and netuid: + print_error("Cannot specify both --netuid and --all-netuids") + raise typer.Exit() + + if not netuid and not all_netuids: + netuid = Prompt.ask( + "Enter the [blue]netuid[/blue] to view the price of [dim](or Press Enter to view all subnets)[/dim]", + ) + if not netuid: + all_netuids = True + html_output = True + + if all_netuids and not html_output: + print_error("Cannot specify --all-netuids without --html") + raise typer.Exit() + return self._run_command( - subnets.price(self.initialize_chain(network), netuid, interval_hours) + price.price(self.initialize_chain(network), netuid, all_netuids, interval_hours, html_output) ) def subnets_show( diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py new file mode 100644 index 00000000..dd693b70 --- /dev/null +++ b/bittensor_cli/src/commands/subnets/price.py @@ -0,0 +1,834 @@ +import asyncio +import json +from pywry import PyWry +from typing import TYPE_CHECKING + +import plotille +import plotly.graph_objects as go + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + get_subnet_name, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def price( + subtensor: "SubtensorInterface", + netuid: int, + all_netuids: bool = False, + interval_hours: int = 24, + html_output: bool = False, +): + if all_netuids: + netuids = [ + nid for nid in await subtensor.get_all_subnet_netuids() if nid != 0 + ] + else: + netuids = [netuid] + + blocks_per_hour = int(3600 / 12) # ~300 blocks per hour + total_blocks = blocks_per_hour * interval_hours + + with console.status(":chart_increasing: Fetching historical price data..."): + current_block_hash = await subtensor.substrate.get_chain_head() + current_block = await subtensor.substrate.get_block_number(current_block_hash) + + step = 300 + start_block = max(0, current_block - total_blocks) + block_numbers = list(range(start_block, current_block + 1, step)) + + # Block hashes + block_hash_cors = [subtensor.substrate.get_block_hash(bn) for bn in block_numbers] + block_hashes = await asyncio.gather(*block_hash_cors) + + # Subnet info + if all_netuids: + subnet_info_cors = [ + subtensor.get_all_subnet_dynamic_info(bh) for bh in block_hashes + ] + else: + # TODO: Add feat supporting multiple netuids + netuid = netuids[0] + subnet_info_cors = [ + subtensor.get_subnet_dynamic_info(netuid, bh) for bh in block_hashes + ] + all_subnet_infos = await asyncio.gather(*subnet_info_cors) + + # 3) Process the data + subnet_data = _process_subnet_data( + block_numbers, all_subnet_infos, netuids, all_netuids, interval_hours + ) + + if not subnet_data: + err_console.print("[red]No valid price data found for any subnet[/red]") + return + + if html_output: + await _generate_html_output(subnet_data, block_numbers, interval_hours) + else: + _generate_cli_output(subnet_data, block_numbers, interval_hours) + + +def _process_subnet_data( + block_numbers, + all_subnet_infos, + netuids, + all_netuids, + interval_hours, +): + """ + Take the fetched subnet data and build a dictionary of + { netuid: { prices, stats } } containing: + - historical prices + - current/high/low + - supply/market_cap/emission/stake, etc. + Returns a dict sorted by market_cap desc. + """ + subnet_data = {} + + if all_netuids: + for netuid in netuids: + prices = [] + valid_subnet_infos = [] + for block_num, subnet_infos in zip(block_numbers, all_subnet_infos): + subnet_info = next((s for s in subnet_infos if s.netuid == netuid), None) + if subnet_info: + prices.append(subnet_info.price.tao) + valid_subnet_infos.append(subnet_info) + + if not valid_subnet_infos or not prices: + # No valid data found for this netuid + continue + + if len(prices) < 5: + err_console.print( + f"[red]Insufficient price data for subnet {netuid}. " + f"Need at least 5 data points but only found {len(prices)}.[/red]" + ) + continue + + # Most recent data for statistics + latest_subnet_data = valid_subnet_infos[-1] + stats = { + "current_price": prices[-1], + "high": max(prices), + "low": min(prices), + "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100), + "supply": latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao, + "market_cap": latest_subnet_data.price.tao + * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao), + "emission": latest_subnet_data.emission.tao, + "stake": latest_subnet_data.alpha_out.tao, + "symbol": latest_subnet_data.symbol, + "name": get_subnet_name(latest_subnet_data), + } + subnet_data[netuid] = { + "prices": prices, + "stats": stats, + } + + else: + prices = [] + valid_subnet_infos = [] + for block_num, subnet_info in zip(block_numbers, all_subnet_infos): + if subnet_info: + prices.append(subnet_info.price.tao) + valid_subnet_infos.append(subnet_info) + + if not valid_subnet_infos or not prices: + err_console.print("[red]No valid price data found for any subnet[/red]") + return {} + + if len(prices) < 5: + err_console.print( + f"[red]Insufficient price data for subnet {netuids[0]}. " + f"Need at least 5 data points but only found {len(prices)}.[/red]" + ) + return {} + + # Most recent data for statistics + latest_subnet_data = valid_subnet_infos[-1] + stats = { + "current_price": prices[-1], + "high": max(prices), + "low": min(prices), + "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100), + "supply": latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao, + "market_cap": latest_subnet_data.price.tao + * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao), + "emission": latest_subnet_data.emission.tao, + "stake": latest_subnet_data.alpha_out.tao, + "symbol": latest_subnet_data.symbol, + "name": get_subnet_name(latest_subnet_data), + } + subnet_data[netuids[0]] = { + "prices": prices, + "stats": stats, + } + + # Sort results by market cap + sorted_subnet_data = dict( + sorted( + subnet_data.items(), + key=lambda x: x[1]["stats"]["market_cap"], + reverse=True, + ) + ) + return sorted_subnet_data + + +def _generate_html_single_subnet( + netuid, + data, + block_numbers, + interval_hours, +): + stats = data["stats"] + prices = data["prices"] + + fig = go.Figure() + fig.add_trace( + go.Scatter( + x=block_numbers, + y=prices, + mode="lines", + name=f"Subnet {netuid} - {stats['name']}" if stats["name"] else f"Subnet {netuid}", + line=dict(width=2, color="#1f77b4"), + ) + ) + + fig.update_layout( + template="plotly_dark", + paper_bgcolor="#000000", + plot_bgcolor="#000000", + font=dict(color="white"), + showlegend=True, + legend=dict( + x=1.02, + y=1.0, + xanchor="left", + yanchor="top", + bgcolor="rgba(0,0,0,0)", + bordercolor="rgba(255,255,255,0.2)", + borderwidth=1, + ), + margin=dict(t=160, r=50, b=50, l=50), + height=600, + ) + + # Label axes + fig.update_xaxes( + title="Block", + gridcolor="rgba(128,128,128,0.2)", + zerolinecolor="rgba(128,128,128,0.2)", + ) + fig.update_yaxes( + title=f"Price ({stats['symbol']})", + gridcolor="rgba(128,128,128,0.2)", + zerolinecolor="rgba(128,128,128,0.2)", + ) + + # Price change color + price_change_class = "text-green" if stats["change_pct"] > 0 else "text-red" + # Change sign + sign_icon = "▲" if stats["change_pct"] > 0 else "▼" + + fig_dict = fig.to_dict() + fig_json = json.dumps(fig_dict) + html_content = f""" + + + + + Subnet Price View + + + + +
+
+
+ {stats['current_price']:.6f} {stats['symbol']} + + {sign_icon} {abs(stats['change_pct']):.2f}% + +
+
+
+ {interval_hours}h High: {stats['high']:.6f} {stats['symbol']} +
+
+ {interval_hours}h Low: {stats['low']:.6f} {stats['symbol']} +
+
+
+
+
Supply: {stats['supply']:.2f} {stats['symbol']}
+
Market Cap: {stats['market_cap']:.2f} τ
+
Emission: {stats['emission']:.2f} {stats['symbol']}
+
Stake: {stats['stake']:.2f} {stats['symbol']}
+
+
+
+ + + + """ + + return html_content + + +def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours): + # Pick top subnet by market cap + top_subnet_id = max( + subnet_data.keys(), + key=lambda k: subnet_data[k]["stats"]["market_cap"], + ) + top_subnet_stats = subnet_data[top_subnet_id]["stats"] + + fig = go.Figure() + fig.update_layout( + template="plotly_dark", + paper_bgcolor="#000000", + plot_bgcolor="#000000", + font=dict(color="white"), + showlegend=True, + legend=dict( + x=1.02, + y=1.0, + xanchor="left", + yanchor="top", + bgcolor="rgba(0,0,0,0)", + bordercolor="rgba(255,255,255,0.2)", + borderwidth=1, + ), + margin=dict(t=200, r=80, b=50, l=50), + height=700, + ) + + # Label axes + fig.update_xaxes( + title="Block", + gridcolor="rgba(128,128,128,0.2)", + zerolinecolor="rgba(128,128,128,0.2)", + ) + fig.update_yaxes( + title="Price (τ)", + gridcolor="rgba(128,128,128,0.2)", + zerolinecolor="rgba(128,128,128,0.2)", + ) + + # Create annotation for top subnet + sign_icon = "▲" if top_subnet_stats["change_pct"] > 0 else "▼" + change_color = "#00FF00" if top_subnet_stats["change_pct"] > 0 else "#FF5555" + + left_text = ( + f"Top subnet: Subnet {top_subnet_id}" + + (f" - {top_subnet_stats['name']}" if top_subnet_stats["name"] else "") + + "

" + + f"{top_subnet_stats['current_price']:.6f} {top_subnet_stats['symbol']}" + + f" {sign_icon} {abs(top_subnet_stats['change_pct']):.2f}%

" + + f"{interval_hours}h High: {top_subnet_stats['high']:.6f}, " + + f"Low: {top_subnet_stats['low']:.6f}" + ) + + right_text = ( + f"Supply: {top_subnet_stats['supply']:.2f} {top_subnet_stats['symbol']}
" + f"Market Cap: {top_subnet_stats['market_cap']:.2f} τ
" + f"Emission: {top_subnet_stats['emission']:.2f} {top_subnet_stats['symbol']}
" + f"Stake: {top_subnet_stats['stake']:.2f} {top_subnet_stats['symbol']}" + ) + + all_annotations = [ + dict( + text=left_text, + x=0.0, + y=1.3, + xref="paper", + yref="paper", + align="left", + showarrow=False, + font=dict(size=14), + xanchor="left", + yanchor="top", + ), + dict( + text=right_text, + x=1.02, + y=1.3, + xref="paper", + yref="paper", + align="left", + showarrow=False, + font=dict(size=14), + xanchor="left", + yanchor="top", + ), + ] + + fig.update_layout(annotations=all_annotations) + + # Generate colors for subnets + def generate_color_palette(n): + """Generate n distinct colors using a variation of HSV color space.""" + colors = [] + for i in range(n): + hue = i * 0.618033988749895 % 1 + saturation = 0.6 + (i % 3) * 0.2 + value = 0.8 + (i % 2) * 0.2 # Brightness + + h = hue * 6 + c = value * saturation + x = c * (1 - abs(h % 2 - 1)) + m = value - c + + if h < 1: + r, g, b = c, x, 0 + elif h < 2: + r, g, b = x, c, 0 + elif h < 3: + r, g, b = 0, c, x + elif h < 4: + r, g, b = 0, x, c + elif h < 5: + r, g, b = x, 0, c + else: + r, g, b = c, 0, x + + rgb = ( + int((r + m) * 255), + int((g + m) * 255), + int((b + m) * 255), + ) + colors.append(f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}") + return colors + + base_colors = generate_color_palette(len(subnet_data) + 1) + + # Plot each subnet as a separate trace + subnet_keys = list(subnet_data.keys()) + for i, netuid in enumerate(subnet_keys): + d = subnet_data[netuid] + fig.add_trace( + go.Scatter( + x=block_numbers, + y=d["prices"], + mode="lines", + name=( + f"Subnet {netuid} - {d['stats']['name']}" + if d["stats"]["name"] + else f"Subnet {netuid}" + ), + line=dict(width=2, color=base_colors[i]), + visible=True, + ) + ) + + # Annotations for each subnet + def build_single_subnet_annotations(netuid): + s = subnet_data[netuid]["stats"] + name_line = f"Subnet {netuid}" + (f" - {s['name']}" if s["name"] else "") + + sign_icon = "▲" if s["change_pct"] > 0 else "▼" + change_color = "#00FF00" if s["change_pct"] > 0 else "#FF5555" + + left_text = ( + f"{name_line}

" + f"{s['current_price']:.6f} {s['symbol']}" + f" {sign_icon} {abs(s['change_pct']):.2f}%

" + f"{interval_hours}h High: {s['high']:.6f}, " + f"Low: {s['low']:.6f}" + ) + + right_text = ( + f"Supply: {s['supply']:.2f} {s['symbol']}
" + f"Market Cap: {s['market_cap']:.2f} τ
" + f"Emission: {s['emission']:.2f} {s['symbol']}
" + f"Stake: {s['stake']:.2f} {s['symbol']}" + ) + + left_annot = dict( + text=left_text, + x=0.0, + y=1.3, + xref="paper", + yref="paper", + align="left", + showarrow=False, + font=dict(size=14), + xanchor="left", + yanchor="top", + ) + right_annot = dict( + text=right_text, + x=1.02, + y=1.3, + xref="paper", + yref="paper", + align="left", + showarrow=False, + font=dict(size=14), + xanchor="left", + yanchor="top", + ) + return [left_annot, right_annot] + + # "All" visibility mask + all_visibility = [True] * len(subnet_keys) + + # Build visibility masks for each subnet + subnet_modes = {} + for idx, netuid in enumerate(subnet_keys): + single_vis = [False] * len(subnet_keys) + single_vis[idx] = True + single_annots = build_single_subnet_annotations(netuid) + subnet_modes[netuid] = { + "visible": single_vis, + "annotations": single_annots, + } + + fig_json = fig.to_json() + all_visibility_json = json.dumps(all_visibility) + all_annotations_json = json.dumps(all_annotations) + + subnet_modes_json = {} + for netuid, mode_data in subnet_modes.items(): + subnet_modes_json[netuid] = { + "visible": json.dumps(mode_data["visible"]), + "annotations": json.dumps(mode_data["annotations"]), + } + + # Build buttons for each subnet + all_button_html = '' + subnet_buttons_html = "" + for netuid in subnet_keys: + subnet_buttons_html += f' ' + + html_content = f""" + + + + Multi-Subnet Price Chart + + + + +
+
+
+ {all_button_html} + {subnet_buttons_html} +
+
+ + + + """ + return html_content + + +async def _generate_html_output( + subnet_data, + block_numbers, + interval_hours, +): + try: + subnet_keys = list(subnet_data.keys()) + + # Single subnet + if len(subnet_keys) == 1: + netuid = subnet_keys[0] + data = subnet_data[netuid] + html_content = _generate_html_single_subnet( + netuid, data, block_numbers, interval_hours + ) + console.print( + "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" + ) + + handler = PyWry() + handler.send_html( + html=html_content, + title=f"Subnet {netuid} Price View", + width=1200, + height=800, + ) + handler.start() + try: + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + handler.close() + + else: + # Multi-subnet + console.print( + "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" + ) + html_content = _generate_html_multi_subnet( + subnet_data, block_numbers, interval_hours + ) + handler = PyWry() + handler.send_html( + html=html_content, + title="Multi-Subnet Price Chart", + width=1200, + height=800, + ) + handler.start() + try: + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + handler.close() + + except ImportError: + console.print( + "[red]Error: Please install plotly to use HTML output: pip install plotly[/red]" + ) + + +def _generate_cli_output(subnet_data, block_numbers, interval_hours): + """ + Render the price data in a textual CLI style with plotille ASCII charts. + """ + for netuid, data in subnet_data.items(): + fig = plotille.Figure() + fig.width = 60 + fig.height = 20 + fig.color_mode = "rgb" + fig.background = None + + def color_label(text): + return plotille.color(text, fg=(186, 233, 143), mode="rgb") + + fig.x_label = color_label("Block") + fig.y_label = color_label(f"Price ({data['stats']['symbol']})") + + fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers)) + fig.set_y_limits( + min_=data["stats"]["low"] * 0.95, + max_=data["stats"]["high"] * 1.05, + ) + + fig.plot( + block_numbers, + data["prices"], + label=f"Subnet {netuid} Price", + interp="linear", + lc="bae98f", + ) + + stats = data["stats"] + change_color = "dark_sea_green3" if stats["change_pct"] > 0 else "red" + + if netuid != 0: + console.print( + f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n" + f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n" + f"{interval_hours}h High: [dark_sea_green3]{stats['high']:.6f}{stats['symbol']}[/dark_sea_green3]\n" + f"{interval_hours}h Low: [red]{stats['low']:.6f}{stats['symbol']}[/red]\n" + f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n" + ) + else: + console.print( + f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n" + f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n" + f"{interval_hours}h High: [dark_sea_green3]{stats['symbol']} {stats['high']:.6f}[/dark_sea_green3]\n" + f"{interval_hours}h Low: [red]{stats['symbol']} {stats['low']:.6f}[/red]\n" + f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n" + ) + + print(fig.show()) + + if netuid != 0: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n" + f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]" + f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n" + f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]" + f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['STAKE']['TAO']}]" + ) + else: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n" + f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]" + f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n" + f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]" + f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE['STAKE']['TAO']}]" + ) + + console.print(stats_text) diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py similarity index 94% rename from bittensor_cli/src/commands/subnets.py rename to bittensor_cli/src/commands/subnets/subnets.py index a8310914..19453353 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -3,7 +3,6 @@ import sqlite3 from typing import TYPE_CHECKING, Optional, cast import typer -import plotille from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError @@ -2051,124 +2050,3 @@ async def metagraph_cmd( table.add_row(*row) console.print(table) - - -async def price( - subtensor: "SubtensorInterface", - netuid: int, - interval_hours: int = 24, -): - """Plot historical subnet price data in the CLI.""" - - blocks_per_hour = int(3600 / 12) # ~300 blocks per hour - total_blocks = blocks_per_hour * interval_hours - - with console.status(":chart_increasing: Fetching historical price data..."): - current_block_hash = await subtensor.substrate.get_chain_head() - current_block = await subtensor.substrate.get_block_number(current_block_hash) - - # Block range - step = 300 - start_block = max(0, current_block - total_blocks) - block_numbers = range(start_block, current_block + 1, step) - - # Fetch all block hashes - block_hash_cors = [ - subtensor.substrate.get_block_hash(bn) for bn in block_numbers - ] - block_hashes = await asyncio.gather(*block_hash_cors) - - # Fetch subnet data for each block - subnet_info_cors = [ - subtensor.get_subnet_dynamic_info(netuid, bh) for bh in block_hashes - ] - subnet_infos = await asyncio.gather(*subnet_info_cors) - - valid_data = [ - (bn, info) - for bn, info in zip(block_numbers, subnet_infos) - if info is not None - ] - if not valid_data: - err_console.print(f"[red]No price data found for subnet {netuid}[/red]") - return - - block_numbers, subnet_infos = zip(*valid_data) - prices = [subnet_info.price.tao for subnet_info in subnet_infos] - - if len(prices) < 5: - err_console.print( - f"[red]Insufficient price data for subnet {netuid}. Need at least 5 data points but only found {len(prices)}.[/red]" - ) - return - - fig = plotille.Figure() - fig.width = 60 - fig.height = 20 - fig.color_mode = "rgb" - fig.background = None - - def color_label(text): - return plotille.color(text, fg=(186, 233, 143), mode="rgb") # Green - - fig.x_label = color_label("Block") - fig.y_label = color_label(f"Price ({Balance.get_unit(netuid)})") - - fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers)) - fig.set_y_limits(min_=min(prices) * 0.95, max_=max(prices) * 1.05) - - fig.plot( - block_numbers, - prices, - label=f"Subnet {netuid} Price", - interp="linear", - lc="bae98f", # Green hex - ) - - subnet = subnet_infos[-1] - console.print( - f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {subnet.symbol} [cyan]{get_subnet_name(subnet)}" - ) - console.print( - f"Current: [blue]{prices[-1]:.6f}{Balance.get_unit(netuid)}" - if netuid != 0 - else f"Current: [blue]{Balance.get_unit(netuid)} {prices[-1]:.6f}" - ) - console.print( - f"{interval_hours}h High: [dark_sea_green3]{max(prices):.6f}{Balance.get_unit(netuid)}" - if netuid != 0 - else f"{interval_hours}h High: [dark_sea_green3]{Balance.get_unit(netuid)} {max(prices):.6f}" - ) - console.print( - f"{interval_hours}h Low: [red]{min(prices):.6f}{Balance.get_unit(netuid)}" - if netuid != 0 - else f"{interval_hours}h Low: [red]{Balance.get_unit(netuid)} {min(prices):.6f}" - ) - - change_color = "dark_sea_green3" if prices[-1] > prices[0] else "red" - console.print( - f"{interval_hours}h Change: " - f"[{change_color}]{((prices[-1] - prices[0]) / prices[0] * 100):.2f}%\n" - ) - print(fig.show()) - console.print("\nLatest stats:") - console.print( - f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet.alpha_in.tao + subnet.alpha_out.tao:,.2f} {Balance.get_unit(netuid)}" - if netuid != 0 - else f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{Balance.get_unit(netuid)} {subnet.alpha_in.tao + subnet.alpha_out.tao:,.2f}" - ) - console.print( - f"Market Cap: [steel_blue3]{subnet.price.tao * (subnet.alpha_in.tao + subnet.alpha_out.tao):,.2f} {Balance.get_unit(netuid)} / 21M" - if netuid != 0 - else f"Market Cap: [steel_blue3]{Balance.get_unit(netuid)} {subnet.price.tao * (subnet.alpha_in.tao + subnet.alpha_out.tao):,.2f} / 21M" - ) - console.print( - f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]{subnet.emission.tao:,.2f} {Balance.get_unit(netuid)}" - if netuid != 0 - else f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]{Balance.get_unit(netuid)} {subnet.emission.tao:,.2f}" - ) - console.print( - f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]{subnet.alpha_out.tao:,.2f} {Balance.get_unit(netuid)}" - if netuid != 0 - else f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]{Balance.get_unit(netuid)} {subnet.alpha_out.tao:,.2f}" - ) diff --git a/requirements.txt b/requirements.txt index f4f62160..c98676e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,6 @@ typer~=0.12 websockets>=14.1 bittensor-wallet>=2.0.2 bt-decode==0.4.0 -plotille \ No newline at end of file +plotille +pywry +plotly From d3fce21179f1d9ee51cd2810699a403595c1870a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 22 Jan 2025 14:49:47 -0800 Subject: [PATCH 224/332] Updates price cmd --- bittensor_cli/cli.py | 50 +++++++++---- bittensor_cli/src/commands/subnets/price.py | 79 ++++++++++++--------- 2 files changed, 83 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 93863f5d..31f79f02 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3562,10 +3562,12 @@ def subnets_list( def subnets_price( self, network: Optional[list[str]] = Options.network, - netuid: Optional[int] = typer.Option( + netuids: str = typer.Option( None, + "--netuids", "--netuid", - help="The netuid of the subnet (e.g. 1)", + "-n", + help="Netuid(s) to show the price for.", ), interval_hours: int = typer.Option( 24, @@ -3584,28 +3586,52 @@ def subnets_price( """ Shows the historical price of a subnet for the past 24 hours. + This command displays the historical price of a subnet for the past 24 hours. + If the `--all` flag is used, the command will display the price for all subnets in html format. + If the `--html` flag is used, the command will display the price in an HTML chart. + If no html flag is used, the command will display the price in the cli. + EXAMPLE [green]$[/green] btcli subnets price --netuid 1 + [green]$[/green] btcli subnets price --all --html + [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ - if all_netuids and netuid: + if netuids: + netuids = parse_to_list( + netuids, + int, + "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3,4`.", + ) + if all_netuids and netuids: print_error("Cannot specify both --netuid and --all-netuids") raise typer.Exit() - - if not netuid and not all_netuids: - netuid = Prompt.ask( - "Enter the [blue]netuid[/blue] to view the price of [dim](or Press Enter to view all subnets)[/dim]", + + if not netuids and not all_netuids: + netuids = Prompt.ask( + "Enter the [blue]netuid(s)[/blue] to view the price of in comma-separated format [dim](or Press Enter to view all subnets)[/dim]", ) - if not netuid: + if not netuids: all_netuids = True html_output = True + else: + netuids = parse_to_list( + netuids, + int, + "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3,4`.", + ) - if all_netuids and not html_output: - print_error("Cannot specify --all-netuids without --html") - raise typer.Exit() + if all_netuids: + html_output = True return self._run_command( - price.price(self.initialize_chain(network), netuid, all_netuids, interval_hours, html_output) + price.price( + self.initialize_chain(network), + netuids, + all_netuids, + interval_hours, + html_output, + ) ) def subnets_show( diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index dd693b70..5783640a 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -11,6 +11,7 @@ console, err_console, get_subnet_name, + print_error, ) if TYPE_CHECKING: @@ -19,17 +20,16 @@ async def price( subtensor: "SubtensorInterface", - netuid: int, + netuids: list[int], all_netuids: bool = False, interval_hours: int = 24, html_output: bool = False, ): + """ + Fetch historical price data for subnets and display it in a chart. + """ if all_netuids: - netuids = [ - nid for nid in await subtensor.get_all_subnet_netuids() if nid != 0 - ] - else: - netuids = [netuid] + netuids = [nid for nid in await subtensor.get_all_subnet_netuids() if nid != 0] blocks_per_hour = int(3600 / 12) # ~300 blocks per hour total_blocks = blocks_per_hour * interval_hours @@ -43,23 +43,24 @@ async def price( block_numbers = list(range(start_block, current_block + 1, step)) # Block hashes - block_hash_cors = [subtensor.substrate.get_block_hash(bn) for bn in block_numbers] + block_hash_cors = [ + subtensor.substrate.get_block_hash(bn) for bn in block_numbers + ] block_hashes = await asyncio.gather(*block_hash_cors) - # Subnet info - if all_netuids: + # Subnet info (we fetch all subnets in-case there are more than one) + if all_netuids or len(netuids) > 1: subnet_info_cors = [ subtensor.get_all_subnet_dynamic_info(bh) for bh in block_hashes ] else: - # TODO: Add feat supporting multiple netuids + # If there is only one netuid, we fetch the subnet info for that netuid netuid = netuids[0] subnet_info_cors = [ subtensor.get_subnet_dynamic_info(netuid, bh) for bh in block_hashes ] all_subnet_infos = await asyncio.gather(*subnet_info_cors) - # 3) Process the data subnet_data = _process_subnet_data( block_numbers, all_subnet_infos, netuids, all_netuids, interval_hours ) @@ -82,21 +83,18 @@ def _process_subnet_data( interval_hours, ): """ - Take the fetched subnet data and build a dictionary of - { netuid: { prices, stats } } containing: - - historical prices - - current/high/low - - supply/market_cap/emission/stake, etc. - Returns a dict sorted by market_cap desc. + Process subnet data into a structured format for price analysis. """ subnet_data = {} - if all_netuids: + if all_netuids or len(netuids) > 1: for netuid in netuids: prices = [] valid_subnet_infos = [] - for block_num, subnet_infos in zip(block_numbers, all_subnet_infos): - subnet_info = next((s for s in subnet_infos if s.netuid == netuid), None) + for _, subnet_infos in zip(block_numbers, all_subnet_infos): + subnet_info = next( + (s for s in subnet_infos if s.netuid == netuid), None + ) if subnet_info: prices.append(subnet_info.price.tao) valid_subnet_infos.append(subnet_info) @@ -111,7 +109,7 @@ def _process_subnet_data( f"Need at least 5 data points but only found {len(prices)}.[/red]" ) continue - + # Most recent data for statistics latest_subnet_data = valid_subnet_infos[-1] stats = { @@ -119,7 +117,8 @@ def _process_subnet_data( "high": max(prices), "low": min(prices), "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100), - "supply": latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao, + "supply": latest_subnet_data.alpha_in.tao + + latest_subnet_data.alpha_out.tao, "market_cap": latest_subnet_data.price.tao * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao), "emission": latest_subnet_data.emission.tao, @@ -135,7 +134,7 @@ def _process_subnet_data( else: prices = [] valid_subnet_infos = [] - for block_num, subnet_info in zip(block_numbers, all_subnet_infos): + for _, subnet_info in zip(block_numbers, all_subnet_infos): if subnet_info: prices.append(subnet_info.price.tao) valid_subnet_infos.append(subnet_info) @@ -158,7 +157,8 @@ def _process_subnet_data( "high": max(prices), "low": min(prices), "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100), - "supply": latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao, + "supply": latest_subnet_data.alpha_in.tao + + latest_subnet_data.alpha_out.tao, "market_cap": latest_subnet_data.price.tao * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao), "emission": latest_subnet_data.emission.tao, @@ -188,6 +188,9 @@ def _generate_html_single_subnet( block_numbers, interval_hours, ): + """ + Generate an HTML chart for a single subnet. + """ stats = data["stats"] prices = data["prices"] @@ -197,7 +200,9 @@ def _generate_html_single_subnet( x=block_numbers, y=prices, mode="lines", - name=f"Subnet {netuid} - {stats['name']}" if stats["name"] else f"Subnet {netuid}", + name=f"Subnet {netuid} - {stats['name']}" + if stats["name"] + else f"Subnet {netuid}", line=dict(width=2, color="#1f77b4"), ) ) @@ -346,12 +351,15 @@ def _generate_html_single_subnet( def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours): + """ + Generate an HTML chart for multiple subnets. + """ # Pick top subnet by market cap - top_subnet_id = max( + top_subnet_netuid = max( subnet_data.keys(), key=lambda k: subnet_data[k]["stats"]["market_cap"], ) - top_subnet_stats = subnet_data[top_subnet_id]["stats"] + top_subnet_stats = subnet_data[top_subnet_netuid]["stats"] fig = go.Figure() fig.update_layout( @@ -390,7 +398,7 @@ def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours): change_color = "#00FF00" if top_subnet_stats["change_pct"] > 0 else "#FF5555" left_text = ( - f"Top subnet: Subnet {top_subnet_id}" + f"Top subnet: Subnet {top_subnet_netuid}" + (f" - {top_subnet_stats['name']}" if top_subnet_stats["name"] else "") + "

" + f"{top_subnet_stats['current_price']:.6f} {top_subnet_stats['symbol']}" @@ -442,7 +450,7 @@ def generate_color_palette(n): for i in range(n): hue = i * 0.618033988749895 % 1 saturation = 0.6 + (i % 3) * 0.2 - value = 0.8 + (i % 2) * 0.2 # Brightness + value = 0.8 + (i % 2) * 0.2 # Brightness h = hue * 6 c = value * saturation @@ -566,7 +574,9 @@ def build_single_subnet_annotations(netuid): } # Build buttons for each subnet - all_button_html = '' + all_button_html = ( + '' + ) subnet_buttons_html = "" for netuid in subnet_keys: subnet_buttons_html += f' ' @@ -697,6 +707,9 @@ async def _generate_html_output( block_numbers, interval_hours, ): + """ + Start PyWry and display the price chart in a window. + """ try: subnet_keys = list(subnet_data.keys()) @@ -747,10 +760,8 @@ async def _generate_html_output( except KeyboardInterrupt: handler.close() - except ImportError: - console.print( - "[red]Error: Please install plotly to use HTML output: pip install plotly[/red]" - ) + except Exception as e: + print_error(f"Error: {e}") def _generate_cli_output(subnet_data, block_numbers, interval_hours): From 02c17de9900a44bb830b126badf8366cc451b516 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 22 Jan 2025 17:00:33 -0800 Subject: [PATCH 225/332] Fixes optional identity fields --- bittensor_cli/src/commands/subnets/subnets.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 19453353..56b08739 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -115,9 +115,15 @@ async def _find_event_attributes_in_extrinsic_receipt( has_identity = any(subnet_identity.values()) if has_identity: identity_data = { - "subnet_name": subnet_identity["subnet_name"].encode(), - "github_repo": subnet_identity["github_repo"].encode(), - "subnet_contact": subnet_identity["subnet_contact"].encode(), + "subnet_name": subnet_identity["subnet_name"].encode() + if subnet_identity.get("subnet_name") + else b"", + "github_repo": subnet_identity["github_repo"].encode() + if subnet_identity.get("github_repo") + else b"", + "subnet_contact": subnet_identity["subnet_contact"].encode() + if subnet_identity.get("subnet_contact") + else b"", } for field, value in identity_data.items(): max_size = 64 # bytes From 0656480d6ace741382ad55ebf8b4a0c55e47eb91 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 22 Jan 2025 17:29:11 -0800 Subject: [PATCH 226/332] Adds init to subnet --- bittensor_cli/src/commands/subnets/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bittensor_cli/src/commands/subnets/__init__.py diff --git a/bittensor_cli/src/commands/subnets/__init__.py b/bittensor_cli/src/commands/subnets/__init__.py new file mode 100644 index 00000000..e69de29b From 81f3f9b8451e608865935365ef9f35de18381bae Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 24 Jan 2025 14:59:47 -0800 Subject: [PATCH 227/332] Adds stake_swap, stake_transfer, updates move, updates price chart, updates subtensor_interface, clean up --- bittensor_cli/cli.py | 363 +++++- bittensor_cli/src/__init__.py | 1 + bittensor_cli/src/bittensor/balances.py | 30 +- bittensor_cli/src/bittensor/chain_data.py | 15 +- .../src/bittensor/subtensor_interface.py | 140 +- bittensor_cli/src/commands/stake/move.py | 992 ++++++++++++++ bittensor_cli/src/commands/stake/stake.py | 1154 +---------------- bittensor_cli/src/commands/subnets/price.py | 119 +- bittensor_cli/src/commands/subnets/subnets.py | 9 +- bittensor_cli/src/commands/sudo.py | 3 +- bittensor_cli/src/commands/wallets.py | 54 +- 11 files changed, 1530 insertions(+), 1350 deletions(-) create mode 100644 bittensor_cli/src/commands/stake/move.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 31f79f02..2fa9ba5a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from bittensor_cli.src.commands import sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets -from bittensor_cli.src.commands.stake import children_hotkeys, stake +from bittensor_cli.src.commands.stake import children_hotkeys, stake, move from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters from bittensor_cli.src.bittensor.utils import ( @@ -42,7 +42,6 @@ is_valid_ss58_address, print_error, validate_chain_endpoint, - retry_prompt, validate_netuid, is_rao_network, get_effective_network, @@ -686,8 +685,14 @@ def __init__(self): "list", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_list) self.stake_app.command( - "move", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + "move", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_move) + self.stake_app.command( + "transfer", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] + )(self.stake_transfer) + self.stake_app.command( + "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] + )(self.stake_swap) # stake-children commands children_app = typer.Typer() @@ -2927,14 +2932,18 @@ def stake_remove( def stake_move( self, - network=Options.network, + network: Optional[list[str]] = Options.network, wallet_name=Options.wallet_name, wallet_path=Options.wallet_path, wallet_hotkey=Options.wallet_hotkey, - origin_netuid: int = typer.Option(help="Origin netuid", prompt=True), - destination_netuid: int = typer.Option(help="Destination netuid", prompt=True), + origin_netuid: Optional[int] = typer.Option( + None, "--origin-netuid", help="Origin netuid" + ), + destination_netuid: Optional[int] = typer.Option( + None, "--dest-netuid", help="Destination netuid" + ), destination_hotkey: Optional[str] = typer.Option( - None, help="Destination hotkey", prompt=False + None, "--dest-ss58", "--dest", help="Destination hotkey", prompt=False ), amount: float = typer.Option( None, @@ -2948,43 +2957,346 @@ def stake_move( prompt: bool = Options.prompt, ): """ - Move Staked TAO to a hotkey from one subnet to another. + Move staked TAO between hotkeys while keeping the same coldkey ownership. + + This command allows you to: + - Move stake from one hotkey to another hotkey + - Move stake between different subnets + - Keep the same coldkey ownership - THe move commands converts the origin subnet's dTao to Tao, and then converts Tao to destination subnet's dTao. + You can specify: + - The origin subnet (--origin-netuid) + - The destination subnet (--dest-netuid) + - The destination hotkey (--dest-hotkey) + - The amount to move (--amount) + + If no arguments are provided, an interactive selection menu will be shown. EXAMPLE [green]$[/green] btcli stake move """ - # TODO: Improve logic of moving stake (dest hotkey) - ask_for = ( - [WO.NAME, WO.PATH] if destination_hotkey else [WO.NAME, WO.HOTKEY, WO.PATH] + console.print( + "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" ) - validate = WV.WALLET if destination_hotkey else WV.WALLET_AND_HOTKEY + if not destination_hotkey: + dest_wallet_or_ss58 = Prompt.ask( + "Enter the [blue]destination wallet[/blue] where destination hotkey is located or [blue]ss58 address[/blue]" + ) + if is_valid_ss58_address(dest_wallet_or_ss58): + destination_hotkey = dest_wallet_or_ss58 + else: + dest_wallet = self.wallet_ask( + dest_wallet_or_ss58, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + destination_hotkey = Prompt.ask( + "Enter the [blue]destination hotkey[/blue] name", + default=dest_wallet.hotkey_str, + ) + destination_wallet = self.wallet_ask( + dest_wallet_or_ss58, + wallet_path, + destination_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + destination_hotkey = destination_wallet.hotkey.ss58_address + else: + if is_valid_ss58_address(destination_hotkey): + destination_hotkey = destination_hotkey + else: + print_error( + "Invalid destination hotkey ss58 address. Please enter a valid ss58 address or wallet name." + ) + raise typer.Exit() + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]origin wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=ask_for, - validate=validate, + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - if not destination_hotkey: - destination_hotkey = wallet.hotkey.ss58_address + + interactive_selection = False + if not wallet_hotkey: + origin_hotkey = Prompt.ask( + "Enter the [blue]origin hotkey[/blue] name or " + "[blue]ss58 address[/blue] where the stake will be moved from " + "[dim](or Press Enter to view existing stakes)[/dim]" + ) + if origin_hotkey == "": + interactive_selection = True + + elif is_valid_ss58_address(origin_hotkey): + origin_hotkey = origin_hotkey + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + origin_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + origin_hotkey = wallet.hotkey.ss58_address + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[], + validate=WV.WALLET_AND_HOTKEY, + ) + origin_hotkey = wallet.hotkey.ss58_address + + if not interactive_selection: + if not origin_netuid: + origin_netuid = IntPrompt.ask( + "Enter the [blue]origin subnet[/blue] (netuid) to move stake from" + ) + + if not destination_netuid: + destination_netuid = IntPrompt.ask( + "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" + ) return self._run_command( - stake.move_stake( + move.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, origin_netuid=origin_netuid, + origin_hotkey=origin_hotkey, destination_netuid=destination_netuid, destination_hotkey=destination_hotkey, amount=amount, stake_all=stake_all, + interactive_selection=interactive_selection, + prompt=prompt, + ) + ) + + def stake_transfer( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + origin_netuid: Optional[int] = typer.Option( + None, + "--origin-netuid", + help="The netuid to transfer stake from", + ), + dest_netuid: Optional[int] = typer.Option( + None, + "--dest-netuid", + help="The netuid to transfer stake to", + ), + dest_ss58: Optional[str] = typer.Option( + None, + "--dest-ss58", + "--dest", + "--dest-coldkey", + help="The destination wallet name or SS58 address to transfer stake to", + ), + amount: float = typer.Option( + None, + "--amount", + "-a", + help="Amount of stake to transfer", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Transfer stake between coldkeys while keeping the same hotkey ownership. + + This command allows you to: + - Transfer stake from one coldkey to another coldkey + - Keep the same hotkey ownership + - Transfer stake between different subnets + + You can specify: + - The origin subnet (--origin-netuid) + - The destination subnet (--dest-netuid) + - The destination wallet/address (--dest) + - The amount to transfer (--amount) + + If no arguments are provided, an interactive selection menu will be shown. + + EXAMPLE + + Transfer 100 TAO from subnet 1 to subnet 2: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --dest wallet2 --amount 100 + + Using SS58 address: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --dest 5FrLxJsyJ5x9n2rmxFwosFraxFCKcXZDngEP9H7qjkKgHLcK --amount 100 + """ + console.print( + "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" + ) + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + if not dest_ss58: + dest_ss58 = Prompt.ask( + "Enter the [blue]destination wallet name[/blue] or [blue]coldkey SS58 address[/blue]" + ) + + if is_valid_ss58_address(dest_ss58): + dest_ss58 = dest_ss58 + else: + dest_wallet = self.wallet_ask( + dest_ss58, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + dest_ss58 = dest_wallet.coldkeypub.ss58_address + + interactive_selection = False + if origin_netuid is None and dest_netuid is None and not amount: + interactive_selection = True + else: + if origin_netuid is None: + origin_netuid = IntPrompt.ask( + "Enter the [blue]origin subnet[/blue] (netuid)" + ) + if not amount: + amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to transfer") + + if dest_netuid is None: + dest_netuid = IntPrompt.ask( + "Enter the [blue]destination subnet[/blue] (netuid)" + ) + + return self._run_command( + move.transfer_stake( + wallet=wallet, + subtensor=self.initialize_chain(network), + origin_netuid=origin_netuid, + dest_netuid=dest_netuid, + dest_coldkey_ss58=dest_ss58, + amount=amount, + interactive_selection=interactive_selection, prompt=prompt, ) ) + def stake_swap( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + origin_netuid: Optional[int] = typer.Option( + None, + "--origin-netuid", + "-o", + "--origin", + help="The netuid to swap stake from", + ), + dest_netuid: Optional[int] = typer.Option( + None, + "--dest-netuid", + "-d", + "--dest", + help="The netuid to swap stake to", + ), + amount: float = typer.Option( + None, + "--amount", + "-a", + help="Amount of stake to swap", + ), + swap_all: bool = typer.Option( + False, + "--swap-all", + "--all", + help="Swap all available stake", + ), + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Swap stake between different subnets while keeping the same coldkey-hotkey pair ownership. + + This command allows you to: + - Move stake from one subnet to another subnet + - Keep the same coldkey ownership + - Keep the same hotkey ownership + + You can specify: + - The origin subnet (--origin-netuid) + - The destination subnet (--dest-netuid) + - The amount to swap (--amount) + + If no arguments are provided, an interactive selection menu will be shown. + + EXAMPLE + + Swap 100 TAO from subnet 1 to subnet 2: + [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 + """ + console.print( + "[dim]This command moves stake from one subnet to another subnet while keeping the same coldkey-hotkey pair.[/dim]" + ) + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + interactive_selection = False + if origin_netuid is None and dest_netuid is None and not amount: + interactive_selection = True + else: + if origin_netuid is None: + origin_netuid = IntPrompt.ask( + "Enter the [blue]origin subnet[/blue] (netuid)" + ) + if dest_netuid is None: + dest_netuid = IntPrompt.ask( + "Enter the [blue]destination subnet[/blue] (netuid)" + ) + if not amount and not swap_all: + amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") + + return self._run_command( + move.swap_stake( + wallet=wallet, + subtensor=self.initialize_chain(network), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=amount, + swap_all=swap_all, + interactive_selection=interactive_selection, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + ) + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, @@ -3581,6 +3893,12 @@ def subnets_price( "--all", help="Show the price for all subnets.", ), + log_scale: bool = typer.Option( + False, + "--log-scale", + "--log", + help="Show the price in log scale.", + ), html_output: bool = Options.html_output, ): """ @@ -3589,11 +3907,13 @@ def subnets_price( This command displays the historical price of a subnet for the past 24 hours. If the `--all` flag is used, the command will display the price for all subnets in html format. If the `--html` flag is used, the command will display the price in an HTML chart. + If the `--log-scale` flag is used, the command will display the price in log scale. If no html flag is used, the command will display the price in the cli. EXAMPLE [green]$[/green] btcli subnets price --netuid 1 + [green]$[/green] btcli subnets price --netuid 1 --html --log [green]$[/green] btcli subnets price --all --html [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ @@ -3631,6 +3951,7 @@ def subnets_price( all_netuids, interval_hours, html_output, + log_scale, ) ) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 09311d0e..ffd5ab26 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -869,6 +869,7 @@ class WalletValidationTypes(Enum): "STAKE": { "STAKE_MGMT": "Stake Management", "CHILD": "Child Hotkeys", + "MOVEMENT": "Stake Movement", }, "SUDO": { "CONFIG": "Subnet Configuration", diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 4a795531..70656e58 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -17,7 +17,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union +from typing import Union, TypedDict from bittensor_cli.src import UNITS @@ -301,3 +301,31 @@ def set_unit(self, netuid: int): self.unit = Balance.get_unit(netuid) self.rao_unit = Balance.get_unit(netuid) return self + + +class FixedPoint(TypedDict): + """ + Represents a fixed point ``U64F64`` number. + Where ``bits`` is a U128 representation of the fixed point number. + + This matches the type of the Alpha shares. + """ + + bits: int + + +def fixed_to_float(fixed: FixedPoint) -> float: + # Currently this is stored as a U64F64 + # which is 64 bits of integer and 64 bits of fractional + uint_bits = 64 + frac_bits = 64 + + data: int = fixed["bits"] + + # Shift bits to extract integer part (assuming 64 bits for integer part) + integer_part = data >> frac_bits + fractional_part = data & (2**frac_bits - 1) + + frac_float = fractional_part / (2**frac_bits) + + return integer_part + frac_float diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 0aebe3ca..97949867 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1035,7 +1035,13 @@ def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: else: alpha_returned = tao.set_unit(self.netuid) slippage = Balance.from_tao(0) - return alpha_returned, slippage + + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + alpha_returned) + if slippage + alpha_returned != 0 + else 0 + ) + return alpha_returned, slippage, slippage_pct_float def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: """ @@ -1063,7 +1069,12 @@ def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: else: tao_returned = alpha.set_unit(0) slippage = Balance.from_tao(0) - return tao_returned, slippage + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + tao_returned) + if slippage + tao_returned != 0 + else 0 + ) + return tao_returned, slippage, slippage_pct_float @dataclass diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 52f4f7b5..d017f151 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -29,7 +29,7 @@ DynamicInfo, ) from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.balances import Balance, FixedPoint, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY from bittensor_cli.src.bittensor.utils import ( ss58_to_vec_u8, @@ -227,7 +227,7 @@ async def get_delegates( else: return [] - async def get_stake_info_for_coldkey( + async def get_stake_for_coldkey( self, coldkey_ss58: str, block_hash: Optional[str] = None, @@ -264,25 +264,61 @@ async def get_stake_info_for_coldkey( except ValueError: bytes_result = bytes.fromhex(hex_bytes_result) - return StakeInfo.list_from_vec_u8(bytes_result) + stakes = StakeInfo.list_from_vec_u8(bytes_result) + return [stake for stake in stakes if stake.stake > 0] async def get_stake_for_coldkey_and_hotkey( - self, hotkey_ss58: str, coldkey_ss58: str, block_hash: Optional[str] + self, + hotkey_ss58: str, + coldkey_ss58: str, + netuid: Optional[int] = None, + block_hash: Optional[str] = None, ) -> Balance: """ - Retrieves stake information associated with a specific coldkey and hotkey. - :param hotkey_ss58: the hotkey SS58 address to query - :param coldkey_ss58: the coldkey SS58 address to query - :param block_hash: the hash of the blockchain block number for the query. - :return: Stake Balance for the given coldkey and hotkey + Returns the stake under a coldkey - hotkey pairing. + + Args: + hotkey_ss58 (str): The SS58 address of the hotkey. + coldkey_ss58 (str): The SS58 address of the coldkey. + netuid (Optional[int]): The subnet ID to filter by. If provided, only returns stake for this specific subnet. + block_hash (Optional[str]): The block hash at which to query the stake information. + + Returns: + Balance: The stake under the coldkey - hotkey pairing. """ - _result = await self.substrate.query( + alpha_shares = await self.substrate.query( + module="SubtensorModule", + storage_function="Alpha", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ) + + hotkey_alpha = await self.substrate.query( module="SubtensorModule", - storage_function="Stake", - params=[hotkey_ss58, coldkey_ss58], + storage_function="TotalHotkeyAlpha", + params=[hotkey_ss58, netuid], block_hash=block_hash, ) - return Balance.from_rao(_result or 0) + + hotkey_shares = await self.substrate.query( + module="SubtensorModule", + storage_function="TotalHotkeyShares", + params=[hotkey_ss58, netuid], + block_hash=block_hash, + ) + + alpha_shares_as_float = fixed_to_float(alpha_shares or 0) + hotkey_shares_as_float = fixed_to_float(hotkey_shares or 0) + + 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 or 0) + + return Balance.from_rao(int(stake)).set_unit(netuid=netuid) + + # Alias + get_stake = get_stake_for_coldkey_and_hotkey async def query_runtime_api( self, @@ -384,11 +420,11 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - sub_stakes = await self.get_stake_info_for_coldkeys( + sub_stakes = await self.get_stake_for_coldkeys( ss58_addresses, block_hash=block_hash ) # Token pricing info - dynamic_info = await self.get_all_subnet_dynamic_info() + dynamic_info = await self.all_subnets() results = {} for ss58, stake_info_list in sub_stakes.items(): @@ -1256,27 +1292,6 @@ async def get_delegates_by_netuid_light( return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet - async def get_subnet_dynamic_info( - self, netuid: int, block_hash: Optional[str] = None - ) -> "DynamicInfo": - hex_bytes_result = await self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_dynamic_info", - params=[netuid], - block_hash=block_hash, - ) - - if hex_bytes_result is None: - return None - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - subnets = DynamicInfo.from_vec_u8(bytes_result) - return subnets - async def get_stake_for_coldkey_and_hotkey_on_netuid( self, hotkey_ss58: str, @@ -1354,7 +1369,7 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( results[hotkey_ss58][netuid] = value return results - async def get_stake_info_for_coldkeys( + async def get_stake_for_coldkeys( self, coldkey_ss58_list: list[str], block_hash: Optional[str] = None ) -> Optional[dict[str, list[StakeInfo]]]: """ @@ -1392,7 +1407,9 @@ async def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore - async def get_all_subnet_dynamic_info(self, block_hash: Optional[str] = None) -> list["DynamicInfo"]: + async def all_subnets( + self, block_hash: Optional[str] = None + ) -> list["DynamicInfo"]: query = await self.substrate.runtime_call( "SubnetInfoRuntimeApi", "get_all_dynamic_info", @@ -1401,47 +1418,16 @@ async def get_all_subnet_dynamic_info(self, block_hash: Optional[str] = None) -> subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) return subnets - async def get_global_weights( - self, netuids: list[int], block_hash: Optional[str] = None - ): - result = await self.substrate.query_multiple( - module="SubtensorModule", - storage_function="GlobalWeight", - params=[netuid for netuid in netuids], + async def subnet( + self, netuid: int, block_hash: Optional[str] = None + ) -> "DynamicInfo": + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[netuid], block_hash=block_hash, ) - return { - netuid: u64_normalized_float(weight) for (netuid, weight) in result.items() - } - - async def get_subnet_tao( - self, netuid: Optional[int] = None, block_hash: Optional[str] = None - ) -> dict[int, Balance]: - """ - Retrieves the total TAO for one or all subnets. - - Args: - netuid: Optional specific netuid to query. If None, returns data for all subnets. - block_hash: Optional block hash to query at. - - Returns: - Dictionary mapping netuid to its total TAO balance - """ - if netuid is not None: - result = await self.substrate.query( - module="SubtensorModule", - storage_function="SubnetTAO", - params=[netuid], - block_hash=block_hash, - ) - return {netuid: Balance.from_rao(result or 0)} - else: - results = await self.substrate.query_map( - module="SubtensorModule", - storage_function="SubnetTAO", - block_hash=block_hash, - ) - return {netuid: Balance.from_rao(tao or 0) async for netuid, tao in results} + return DynamicInfo.from_vec_u8(bytes.fromhex(query.decode()[2:])) async def get_owned_hotkeys( self, diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py new file mode 100644 index 00000000..dcf9e3a0 --- /dev/null +++ b/bittensor_cli/src/commands/stake/move.py @@ -0,0 +1,992 @@ +import asyncio + +from typing import TYPE_CHECKING +import typer + +from bittensor_wallet import Wallet +from bittensor_wallet.errors import KeyFileError +from rich.table import Table +from rich.prompt import Confirm, Prompt + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + print_error, + format_error_message, + group_subnets, + get_subnet_name, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + +MIN_STAKE_FEE = Balance.from_rao(500_000) + + +# Helpers +async def display_stake_movement_cross_subnets( + subtensor: "SubtensorInterface", + origin_netuid: int, + destination_netuid: int, + origin_hotkey: str, + destination_hotkey: str, + amount_to_move: Balance, +) -> tuple[Balance, float, str, str]: + """Calculate and display slippage information""" + + if origin_netuid == destination_netuid: + subnet = await subtensor.subnet(origin_netuid) + received_amount_tao = subnet.alpha_to_tao(amount_to_move) + received_amount_tao -= MIN_STAKE_FEE + received_amount = subnet.tao_to_alpha(received_amount_tao) + slippage_pct_float = ( + 100 * float(MIN_STAKE_FEE) / float(MIN_STAKE_FEE + received_amount_tao) + if received_amount_tao != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f}%" + price = Balance.from_tao(1).set_unit(origin_netuid) + price_str = ( + str(float(price.tao)) + + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + else: + dynamic_origin, dynamic_destination = await asyncio.gather( + subtensor.subnet(origin_netuid), + subtensor.subnet(destination_netuid), + ) + price = ( + float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) + ) + received_amount_tao, _, slippage_pct_float = ( + dynamic_origin.alpha_to_tao_with_slippage(amount_to_move) + ) + received_amount_tao -= MIN_STAKE_FEE + received_amount, _, slippage_pct_float = ( + dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) + ) + received_amount.set_unit(destination_netuid) + slippage_pct = f"{slippage_pct_float:.4f} %" + price_str = ( + str(float(price)) + + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + + # Create and display table + table = Table( + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"Moving stake from: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"to: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n" + f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + ), + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column( + "origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + "dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"amount ({Balance.get_unit(origin_netuid)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["TAO"], + ) + table.add_column( + f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"received ({Balance.get_unit(destination_netuid)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) + + table.add_row( + f"{Balance.get_unit(origin_netuid)}({origin_netuid})", + f"{origin_hotkey[:3]}...{origin_hotkey[-3:]}", + f"{Balance.get_unit(destination_netuid)}({destination_netuid})", + f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}", + str(amount_to_move), + price_str, + str(received_amount), + str(slippage_pct), + ) + + console.print(table) + # console.print( + # f"[dim]A fee of {MIN_STAKE_FEE} applies.[/dim]" + # ) + + # Display slippage warning if necessary + if slippage_pct_float > 5: + message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + return received_amount, slippage_pct_float, slippage_pct, price_str + + +def prompt_stake_amount( + current_balance: Balance, netuid: int, action_name: str +) -> tuple[Balance, bool]: + """Prompts user to input a stake amount with validation. + + Args: + current_balance (Balance): The maximum available balance + netuid (int): The subnet id to get the correct unit + action_name (str): The name of the action (e.g. "transfer", "move", "unstake") + + Returns: + tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected) + """ + while True: + amount_input = Prompt.ask( + f"\nEnter amount to {action_name} from " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"or " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"for entire balance" + ) + + if amount_input.lower() == "all": + return current_balance, True + + try: + amount = float(amount_input) + if amount <= 0: + console.print("[red]Amount must be greater than 0[/red]") + continue + if amount > current_balance.tao: + console.print( + f"[red]Amount exceeds available balance of " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"[/red]" + ) + continue + return Balance.from_tao(amount), False + except ValueError: + console.print("[red]Please enter a valid number or 'all'[/red]") + + +async def stake_move_selection( + subtensor: "SubtensorInterface", + wallet: Wallet, +): + """Selection interface for moving stakes between hotkeys and subnets.""" + stakes, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + + hotkey_stakes = {} + for stake in stakes: + if stake.stake.tao > 0: + hotkey = stake.hotkey_ss58 + netuid = stake.netuid + stake_balance = stake.stake + hotkey_stakes.setdefault(hotkey, {})[netuid] = stake_balance + + if not hotkey_stakes: + print_error("You have no stakes to move.") + raise typer.Exit() + + # Display hotkeys with stakes + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Index", justify="right") + table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + + hotkeys_info = [] + for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): + if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58): + hotkey_name = hk_identity.get("identity", {}).get( + "name", "" + ) or hk_identity.get("display", "~") + elif old_identity := old_identities.get(hotkey_ss58): + hotkey_name = old_identity.display + else: + hotkey_name = "~" + hotkeys_info.append( + { + "index": idx, + "identity": hotkey_name, + "hotkey_ss58": hotkey_ss58, + "netuids": list(netuid_stakes.keys()), + "stakes": netuid_stakes, + } + ) + table.add_row( + str(idx), + hotkey_name, + group_subnets([n for n in netuid_stakes.keys()]), + hotkey_ss58, + ) + + console.print("\n", table) + + # Select origin hotkey + origin_idx = Prompt.ask( + "\nEnter the index of the hotkey you want to move stake from", + choices=[str(i) for i in range(len(hotkeys_info))], + ) + origin_hotkey_info = hotkeys_info[int(origin_idx)] + origin_hotkey_ss58 = origin_hotkey_info["hotkey_ss58"] + + # Display available netuids for selected hotkey + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{origin_hotkey_ss58}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n", + show_edge=False, + header_style="bold white", + border_style="bright_black", + title_justify="center", + width=len(origin_hotkey_ss58) + 20, + ) + table.add_column("Index", justify="right") + table.add_column("Netuid", style="cyan") + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + + available_netuids = [] + for idx, netuid in enumerate(origin_hotkey_info["netuids"]): + stake = origin_hotkey_info["stakes"][netuid] + if stake.tao > 0: + available_netuids.append(netuid) + table.add_row(str(idx), str(netuid), str(stake)) + + console.print("\n", table) + + # Select origin netuid + netuid_idx = Prompt.ask( + "\nEnter the index of the subnet you want to move stake from", + choices=[str(i) for i in range(len(available_netuids))], + ) + origin_netuid = available_netuids[int(netuid_idx)] + origin_stake = origin_hotkey_info["stakes"][origin_netuid] + + # Ask for amount to move + amount, stake_all = prompt_stake_amount(origin_stake, origin_netuid, "move") + + all_subnets = sorted(await subtensor.get_all_subnet_netuids()) + destination_netuid = Prompt.ask( + "\nEnter the netuid of the subnet you want to move stake to" + + f" ([dim]{group_subnets(all_subnets)}[/dim])", + choices=[str(netuid) for netuid in all_subnets], + show_choices=False, + ) + + return { + "origin_hotkey": origin_hotkey_ss58, + "origin_netuid": origin_netuid, + "amount": amount.tao, + "stake_all": stake_all, + "destination_netuid": int(destination_netuid), + } + + +async def stake_transfer_selection( + wallet: Wallet, + subtensor: "SubtensorInterface", +): + """Selection interface for transferring stakes.""" + ( + stakes, + all_netuids, + all_subnets, + ) = await asyncio.gather( + subtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address), + subtensor.get_all_subnet_netuids(), + subtensor.all_subnets(), + ) + all_netuids = sorted(all_netuids) + all_subnets = {di.netuid: di for di in all_subnets} + + available_stakes = {} + for stake in stakes: + if stake.stake.tao > 0 and stake.hotkey_ss58 == wallet.hotkey.ss58_address: + available_stakes[stake.netuid] = { + "hotkey_ss58": stake.hotkey_ss58, + "stake": stake.stake, + "is_registered": stake.is_registered, + } + + if not available_stakes: + console.print("[red]No stakes available to transfer.[/red]") + return None + + table = Table( + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"Available Stakes to Transfer\n" + f"for wallet hotkey:\n" + f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}" + f"[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" + ), + show_edge=False, + header_style="bold white", + border_style="bright_black", + title_justify="center", + width=len(wallet.hotkey_str + wallet.hotkey.ss58_address) + 10, + ) + + table.add_column("Index", justify="right", style="cyan") + table.add_column("Netuid") + table.add_column("Name", style="cyan", justify="left") + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column("Registered", justify="center") + + for idx, (netuid, stake_info) in enumerate(available_stakes.items()): + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{all_subnets[netuid].symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {get_subnet_name(all_subnets[netuid])}" + ) + table.add_row( + str(idx), + str(netuid), + subnet_name_cell, + str(stake_info["stake"]), + "[dark_sea_green3]YES" + if stake_info["is_registered"] + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", + ) + + console.print(table) + + if not available_stakes: + console.print("[red]No stakes available to transfer.[/red]") + return None + + # Prompt to select index of stake to transfer + selection = Prompt.ask( + "\nEnter the index of the stake you want to transfer", + choices=[str(i) for i in range(len(available_stakes))], + ) + selected_netuid = list(available_stakes.keys())[int(selection)] + selected_stake = available_stakes[selected_netuid] + + # Prompt for amount + stake_balance = selected_stake["stake"] + amount, _ = prompt_stake_amount(stake_balance, selected_netuid, "transfer") + + # Prompt for destination subnet + destination_netuid = Prompt.ask( + "\nEnter the netuid of the subnet you want to move stake to" + + f" ([dim]{group_subnets(all_netuids)}[/dim])", + choices=[str(netuid) for netuid in all_netuids], + show_choices=False, + ) + + return { + "origin_netuid": selected_netuid, + "amount": amount.tao, + "destination_netuid": int(destination_netuid), + } + + +async def stake_swap_selection( + subtensor: "SubtensorInterface", + wallet: Wallet, +) -> dict: + """Selection interface for swapping stakes between subnets.""" + block_hash = await subtensor.substrate.get_chain_head() + stakes, all_subnets = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.all_subnets(block_hash=block_hash), + ) + subnet_dict = {di.netuid: di for di in all_subnets} + + # Filter stakes for this hotkey + hotkey_stakes = {} + for stake in stakes: + if stake.hotkey_ss58 == wallet.hotkey.ss58_address and stake.stake.tao > 0: + hotkey_stakes[stake.netuid] = { + "stake": stake.stake, + "is_registered": stake.is_registered, + } + + if not hotkey_stakes: + print_error(f"No stakes found for hotkey: {wallet.hotkey_str}") + raise typer.Exit() + + # Display available stakes + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n", + show_edge=False, + header_style="bold white", + border_style="bright_black", + title_justify="center", + width=len(wallet.hotkey.ss58_address) + 20, + ) + + table.add_column("Index", justify="right", style="cyan") + table.add_column("Netuid", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Name", style="cyan", justify="left") + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column("Registered", justify="center") + + available_netuids = [] + for idx, (netuid, stake_info) in enumerate(sorted(hotkey_stakes.items())): + subnet_info = subnet_dict[netuid] + subnet_name_cell = ( + f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f" {get_subnet_name(subnet_info)}" + ) + + available_netuids.append(netuid) + table.add_row( + str(idx), + str(netuid), + subnet_name_cell, + str(stake_info["stake"]), + "[dark_sea_green3]YES" + if stake_info["is_registered"] + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", + ) + + console.print("\n", table) + + # Select origin netuid + origin_idx = Prompt.ask( + "\nEnter the index of the subnet you want to swap stake from", + choices=[str(i) for i in range(len(available_netuids))], + ) + origin_netuid = available_netuids[int(origin_idx)] + origin_stake = hotkey_stakes[origin_netuid]["stake"] + + # Ask for amount to swap + amount, all_balance = prompt_stake_amount(origin_stake, origin_netuid, "swap") + + all_netuids = sorted(await subtensor.get_all_subnet_netuids()) + destination_choices = [ + str(netuid) for netuid in all_netuids if netuid != origin_netuid + ] + destination_netuid = Prompt.ask( + "\nEnter the netuid of the subnet you want to swap stake to" + + f" ([dim]{group_subnets(all_netuids)}[/dim])", + choices=destination_choices, + show_choices=False, + ) + + return { + "origin_netuid": origin_netuid, + "amount": amount.tao, + "destination_netuid": int(destination_netuid), + } + + +# Commands +async def move_stake( + subtensor: "SubtensorInterface", + wallet: Wallet, + origin_netuid: int, + origin_hotkey: str, + destination_netuid: int, + destination_hotkey: str, + amount: float, + stake_all: bool, + interactive_selection: bool = False, + prompt: bool = True, +): + if interactive_selection: + selection = await stake_move_selection(subtensor, wallet) + origin_hotkey = selection["origin_hotkey"] + origin_netuid = selection["origin_netuid"] + amount = selection["amount"] + stake_all = selection["stake_all"] + destination_netuid = selection["destination_netuid"] + + # Get the wallet stake balances. + block_hash = await subtensor.substrate.get_chain_head() + origin_stake_balance, destination_stake_balance = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + block_hash=block_hash, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + block_hash=block_hash, + ), + ) + + if origin_stake_balance.tao == 0: + print_error( + f"Your balance is " + f"[{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] " + f"in Netuid: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + ) + raise typer.Exit() + + console.print( + f"\nOrigin Netuid: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"Origin stake: " + f"[{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]" + ) + console.print( + f"Destination netuid: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"Destination stake: " + f"[{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]\n" + ) + + # Determine the amount we are moving. + amount_to_move_as_balance = None + if amount: + amount_to_move_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_move_as_balance = origin_stake_balance + else: + amount_to_move_as_balance, _ = prompt_stake_amount( + origin_stake_balance, origin_netuid, "move" + ) + + # Check enough to move. + amount_to_move_as_balance.set_unit(origin_netuid) + if amount_to_move_as_balance > origin_stake_balance: + err_console.print( + f"[red]Not enough stake[/red]:\n" + f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + ) + return False + + # Slippage warning + if prompt: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_hotkey=origin_hotkey, + destination_hotkey=destination_hotkey, + amount_to_move=amount_to_move_as_balance, + ) + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Perform moving operation. + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + with console.status( + f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: [blue]{origin_netuid}[/blue] \nto " + f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_move_as_balance.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + else: + await response.process_events() + if not await response.is_success: + err_console.print( + f"\n:cross_mark: [red]Failed[/red] with error:" + f" {format_error_message( await response.error_message, subtensor.substrate)}" + ) + return + else: + console.print( + ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" + ) + block_hash = await subtensor.substrate.get_chain_head() + ( + new_origin_stake_balance, + new_destination_stake_balance, + ) = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + block_hash=block_hash, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + block_hash=block_hash, + ), + ) + + console.print( + f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" + ) + console.print( + f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" + ) + return + + +async def transfer_stake( + wallet: Wallet, + subtensor: "SubtensorInterface", + amount: float, + origin_netuid: int, + dest_netuid: int, + dest_coldkey_ss58: str, + interactive_selection: bool = False, + prompt: bool = True, +) -> bool: + """Transfers stake from one network to another. + + Args: + wallet (Wallet): Bittensor wallet object. + subtensor (SubtensorInterface): Subtensor interface instance. + amount (float): Amount to transfer. + origin_netuid (int): The netuid to transfer stake from. + dest_netuid (int): The netuid to transfer stake to. + dest_coldkey_ss58 (str): The destination coldkey to transfer stake to. + interactive_selection (bool): If true, prompts for selection of origin and destination subnets. + prompt (bool): If true, prompts for confirmation before executing transfer. + + Returns: + bool: True if transfer was successful, False otherwise. + """ + if interactive_selection: + selection = await stake_transfer_selection(wallet, subtensor) + origin_netuid = selection["origin_netuid"] + amount = selection["amount"] + dest_netuid = selection["destination_netuid"] + + # Check if both subnets exist + block_hash = await subtensor.substrate.get_chain_head() + dest_exists, origin_exists = await asyncio.gather( + subtensor.subnet_exists(netuid=dest_netuid, block_hash=block_hash), + subtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash), + ) + if not dest_exists: + err_console.print(f"[red]Subnet {dest_netuid} does not exist[/red]") + return False + + if not origin_exists: + err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") + return False + + # Get current stake balances + hotkey_ss58 = wallet.hotkey.ss58_address + with console.status(f"Retrieving stake data from {subtensor.network}..."): + current_stake = await subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ) + current_dest_stake = await subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=dest_netuid, + ) + amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid) + + # Check if enough stake to transfer + if amount_to_transfer > current_stake: + err_console.print( + f"[red]Not enough stake to transfer[/red]:\n" + f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < " + f"Transfer amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_transfer}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + ) + return False + + # Slippage warning + if prompt: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey=hotkey_ss58, + destination_hotkey=hotkey_ss58, + amount_to_move=amount_to_transfer, + ) + + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Perform transfer operation + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status("\n:satellite: Transferring stake ..."): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": dest_coldkey_ss58, + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": dest_netuid, + "alpha_amount": amount_to_transfer.rao, + }, + ) + + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}" + ) + return False + + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=dest_netuid, + ), + ) + + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" + ) + return True + + +async def swap_stake( + wallet: Wallet, + subtensor: "SubtensorInterface", + origin_netuid: int, + destination_netuid: int, + amount: float, + swap_all: bool = False, + interactive_selection: bool = False, + prompt: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> bool: + """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. + + Args: + wallet (Wallet): The wallet to swap stake from. + subtensor (SubtensorInterface): Subtensor interface instance. + hotkey_ss58 (str): The SS58 address of the hotkey whose stake is being swapped. + origin_netuid (int): The netuid from which stake is removed. + destination_netuid (int): The netuid to which stake is added. + amount (float): The amount to swap. + interactive_selection (bool): If true, prompts for selection of origin and destination subnets. + prompt (bool): If true, prompts for confirmation before executing swap. + wait_for_inclusion (bool): If true, waits for the transaction to be included in a block. + wait_for_finalization (bool): If true, waits for the transaction to be finalized. + + Returns: + bool: True if the swap was successful, False otherwise. + """ + hotkey_ss58 = wallet.hotkey.ss58_address + if interactive_selection: + selection = await stake_swap_selection(subtensor, wallet) + origin_netuid = selection["origin_netuid"] + amount = selection["amount"] + destination_netuid = selection["destination_netuid"] + + # Check if both subnets exist + block_hash = await subtensor.substrate.get_chain_head() + dest_exists, origin_exists = await asyncio.gather( + subtensor.subnet_exists(netuid=destination_netuid, block_hash=block_hash), + subtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash), + ) + if not dest_exists: + err_console.print(f"[red]Subnet {destination_netuid} does not exist[/red]") + return False + + if not origin_exists: + err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") + return False + + # Get current stake balances + with console.status(f"Retrieving stake data from {subtensor.network}..."): + current_stake = await subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ) + current_dest_stake = await subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ) + + if swap_all: + amount_to_swap = Balance.from_tao(current_stake).set_unit(origin_netuid) + else: + amount_to_swap = Balance.from_tao(amount).set_unit(origin_netuid) + + # Check if enough stake to swap + if amount_to_swap > current_stake: + err_console.print( + f"[red]Not enough stake to swap[/red]:\n" + f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < " + f"Swap amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_swap}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + ) + return False + + # Slippage warning + if prompt: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_hotkey=hotkey_ss58, + destination_hotkey=hotkey_ss58, + amount_to_move=amount_to_swap, + ) + + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Perform swap operation + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status( + f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] to netuid [blue]{destination_netuid}[/blue]..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_stake", + call_params={ + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_swap.rao, + }, + ) + + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + + response = await subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}" + ) + return False + + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) + + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" + ) + return True diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index d27719b2..bd95a0e5 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1,7 +1,7 @@ import asyncio from functools import partial -from typing import TYPE_CHECKING, Optional, Sequence, Union, cast +from typing import TYPE_CHECKING, Optional import typer from bittensor_wallet import Wallet @@ -25,7 +25,6 @@ print_error, get_hotkey_wallets_for_wallet, is_valid_ss58_address, - u16_normalized_float, format_error_message, group_subnets, millify_tao, @@ -36,797 +35,6 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -# Helpers and Extrinsics - - -async def _get_threshold_amount( - subtensor: "SubtensorInterface", block_hash: str -) -> Balance: - mrs = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="NominatorMinRequiredStake", - block_hash=block_hash, - ) - min_req_stake: Balance = Balance.from_rao(mrs) - return min_req_stake - - -async def _check_threshold_amount( - subtensor: "SubtensorInterface", - sb: Balance, - block_hash: str, - min_req_stake: Optional[Balance] = None, -) -> tuple[bool, Balance]: - """ - Checks if the new stake balance will be above the minimum required stake threshold. - - :param sb: the balance to check for threshold limits. - - :return: (success, threshold) - `True` if the staking balance is above the threshold, or `False` if the staking balance is below the - threshold. - The threshold balance required to stake. - """ - if not min_req_stake: - min_req_stake = await _get_threshold_amount(subtensor, block_hash) - - if min_req_stake > sb: - return False, min_req_stake - else: - return True, min_req_stake - - -async def add_stake_extrinsic( - subtensor: "SubtensorInterface", - wallet: Wallet, - old_balance: Balance, - hotkey_ss58: Optional[str] = None, - amount: Optional[Balance] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """ - Adds the specified amount of stake to passed hotkey `uid`. - - :param subtensor: the initialized SubtensorInterface object to use - :param wallet: Bittensor wallet object. - :param old_balance: the balance prior to the staking - :param hotkey_ss58: The `ss58` address of the hotkey account to stake to defaults to the wallet's hotkey. - :param amount: Amount to stake as Bittensor balance, `None` if staking all. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - - # Decrypt keys, - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - # Default to wallet's own hotkey if the value is not passed. - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Flag to indicate if we are using the wallet's own hotkey. - own_hotkey: bool - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - block_hash = await subtensor.substrate.get_chain_head() - # Get hotkey owner - print_verbose("Confirming hotkey owner", status) - hotkey_owner = await subtensor.get_hotkey_owner( - hotkey_ss58=hotkey_ss58, block_hash=block_hash - ) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # This is not the wallet's own hotkey, so we are delegating. - if not await subtensor.is_hotkey_delegate( - hotkey_ss58, block_hash=block_hash - ): - err_console.print( - f"Hotkey {hotkey_ss58} is not a delegate on the chain." - ) - return False - - # Get hotkey take - hk_result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Delegates", - params=[hotkey_ss58], - block_hash=block_hash, - ) - hotkey_take = u16_normalized_float(hk_result or 0) - else: - hotkey_take = None - - # Get current stake - print_verbose("Fetching current stake", status) - old_stake = await subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block_hash=block_hash, - ) - - print_verbose("Fetching existential deposit", status) - # Grab the existential deposit. - existential_deposit = await subtensor.get_existential_deposit() - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - else: - staking_balance = Balance.from_tao(amount) - - # Leave existential balance to keep key alive. - if staking_balance > old_balance - existential_deposit: - # If we are staking all, we need to leave at least the existential deposit. - staking_balance = old_balance - existential_deposit - else: - staking_balance = staking_balance - - # Check enough to stake. - if staking_balance > old_balance: - err_console.print( - f":cross_mark: [red]Not enough stake[/red]:[bold white]\n" - f"\tbalance:\t{old_balance}\n" - f"\tamount:\t{staking_balance}\n" - f"\tcoldkey:\t{wallet.name}[/bold white]" - ) - return False - - # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold. - if not own_hotkey: - new_stake_balance = old_stake + staking_balance - print_verbose("Fetching threshold amount") - is_above_threshold, threshold = await _check_threshold_amount( - subtensor, new_stake_balance, block_hash - ) - if not is_above_threshold: - err_console.print( - f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination" - f" stake threshold {threshold}.[/red]" - ) - return False - - # Ask before moving on. - if prompt: - if not own_hotkey: - # We are delegating. - if not Confirm.ask( - f"Do you want to delegate:[bold white]\n" - f"\tamount: {staking_balance}\n" - f"\tto: {hotkey_ss58}\n" - f"\ttake: {hotkey_take}\n[/bold white]" - f"\towner: {hotkey_owner}\n" - ): - return False - else: - if not Confirm.ask( - f"Do you want to stake:[bold white]\n" - f"\tamount: {staking_balance}\n" - f"\tto: {wallet.hotkey_str}\n" - f"\taddress: {hotkey_ss58}[/bold white]\n" - ): - return False - - with console.status( - f":satellite: Staking to: [bold white]{subtensor}[/bold white] ...", - spinner="earth", - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": hotkey_ss58, "amount_staked": staking_balance.rao}, - ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with console.status( - f":satellite: Checking Balance on: [white]{subtensor}[/white] ..." - ): - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block_hash=new_block_hash, - ), - ) - - console.print( - f"Balance:\n" - f"\t[blue]{old_balance}[/blue] :arrow_right: " - f"[green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" - ) - console.print( - f"Stake:\n" - f"\t[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) - return True - else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False - - -async def add_stake_multiple_extrinsic( - subtensor: "SubtensorInterface", - wallet: Wallet, - old_balance: Balance, - hotkey_ss58s: list[str], - amounts: Optional[list[Balance]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. - - :param subtensor: The initialized SubtensorInterface object. - :param wallet: Bittensor wallet object for the coldkey. - :param old_balance: The balance of the wallet prior to staking. - :param hotkey_ss58s: List of hotkeys to stake to. - :param amounts: List of amounts to stake. If `None`, stake all to the first hotkey. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: success: `True` if extrinsic was finalized or included in the block. `True` if any wallet was staked. If - we did not wait for finalization/inclusion, the response is `True`. - """ - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - new_amounts: Sequence[Optional[Balance]] - if amounts is None: - new_amounts = [None] * len(hotkey_ss58s) - else: - new_amounts = [Balance.from_tao(amount) for amount in amounts] - if sum(amount.tao for amount in new_amounts) == 0: - # Staking 0 tao - return True - - # Decrypt coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ..." - ): - block_hash = await subtensor.substrate.get_chain_head() - old_stakes = await asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hk, wallet.coldkeypub.ss58_address, block_hash=block_hash - ) - for hk in hotkey_ss58s - ] - ) - - # Remove existential balance to keep key alive. - ## Keys must maintain a balance of at least 1000 rao to stay alive. - total_staking_rao = sum( - [amount.rao if amount is not None else 0 for amount in new_amounts] - ) - if total_staking_rao == 0: - # Staking all to the first wallet. - if old_balance.rao > 1000: - old_balance -= Balance.from_rao(1000) - - elif total_staking_rao < 1000: - # Staking less than 1000 rao to the wallets. - pass - else: - # Staking more than 1000 rao to the wallets. - ## Reduce the amount to stake to each wallet to keep the balance above 1000 rao. - percent_reduction = 1 - (1000 / total_staking_rao) - new_amounts = [ - Balance.from_tao(amount.tao * percent_reduction) - for amount in cast(Sequence[Balance], new_amounts) - ] - - successful_stakes = 0 - for idx, (hotkey_ss58, amount, old_stake) in enumerate( - zip(hotkey_ss58s, new_amounts, old_stakes) - ): - staking_all = False - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - staking_all = True - else: - # Amounts are cast to balance earlier in the function - assert isinstance(amount, Balance) - staking_balance = amount - - # Check enough to stake - if staking_balance > old_balance: - err_console.print( - f":cross_mark: [red]Not enough balance[/red]:" - f" [green]{old_balance}[/green] to stake: [blue]{staking_balance}[/blue]" - f" from coldkey: [white]{wallet.name}[/white]" - ) - continue - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"Do you want to stake:\n" - f"\t[bold white]amount: {staking_balance}\n" - f"\thotkey: {wallet.hotkey_str}[/bold white ]?" - ): - continue - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": hotkey_ss58, "amount_staked": staking_balance.rao}, - ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_query = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="TxRateLimit", - block_hash=block_hash, - ) - tx_rate_limit_blocks: int = tx_query - if tx_rate_limit_blocks > 0: - with console.status( - f":hourglass: [yellow]Waiting for tx rate limit:" - f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ): - await asyncio.sleep( - tx_rate_limit_blocks * 12 - ) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - old_balance -= staking_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - continue - - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - - new_block_hash = await subtensor.substrate.get_chain_head() - new_stake, new_balance_ = await asyncio.gather( - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block_hash=new_block_hash, - ), - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - console.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, old_stake, new_stake - ) - ) - old_balance = new_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - continue - - if successful_stakes != 0: - with console.status( - f":satellite: Checking Balance on: ([white]{subtensor}[/white] ..." - ): - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, reuse_block=False - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - console.print( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - return True - - return False - - -async def unstake_extrinsic( - subtensor: "SubtensorInterface", - wallet: Wallet, - hotkey_ss58: Optional[str] = None, - amount: Optional[Balance] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Removes stake into the wallet coldkey from the specified hotkey ``uid``. - - :param subtensor: the initialized SubtensorInterface object to use - :param wallet: Bittensor wallet object. - :param hotkey_ss58: The `ss58` address of the hotkey to unstake from. By default, the wallet hotkey is used. - :param amount: Amount to stake as Bittensor balance, or `None` is unstaking all - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - # Decrypt keys, - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching balance and stake", status) - block_hash = await subtensor.substrate.get_chain_head() - old_balance, old_stake, hotkey_owner = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block_hash=block_hash, - ), - subtensor.get_hotkey_owner(hotkey_ss58, block_hash), - ) - - own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner - - # Convert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - else: - unstaking_balance = Balance.from_tao(amount) - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - err_console.print( - f":cross_mark: [red]Not enough stake[/red]: " - f"[green]{stake_on_uid}[/green] to unstake: " - f"[blue]{unstaking_balance}[/blue] from hotkey:" - f" [white]{wallet.hotkey_str}[/white]" - ) - return False - - print_verbose("Fetching threshold amount") - # If nomination stake, check threshold. - if not own_hotkey and not await _check_threshold_amount( - subtensor=subtensor, - sb=(stake_on_uid - unstaking_balance), - block_hash=block_hash, - ): - console.print( - ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"Do you want to unstake:\n" - f"[bold white]\tamount: {unstaking_balance}\n" - f"\thotkey: {wallet.hotkey_str}[/bold white ]?" - ): - return False - - with console.status( - f":satellite: Unstaking from chain: [white]{subtensor}[/white] ...", - spinner="earth", - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_unstaked": unstaking_balance.rao, - }, - ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with console.status( - f":satellite: Checking Balance on: [white]{subtensor}[/white] ..." - ): - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58, wallet.coldkeypub.ss58_address, new_block_hash - ), - ) - console.print( - f"Balance:\n" - f" [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right:" - f" [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" - ) - console.print( - f"Stake:\n [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) - return True - else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False - - -async def unstake_multiple_extrinsic( - subtensor: "SubtensorInterface", - wallet: Wallet, - hotkey_ss58s: list[str], - amounts: Optional[list[Union[Balance, float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """ - Removes stake from each `hotkey_ss58` in the list, using each amount, to a common coldkey. - - :param subtensor: the initialized SubtensorInterface object to use - :param wallet: The wallet with the coldkey to unstake to. - :param hotkey_ss58s: List of hotkeys to unstake from. - :param amounts: List of amounts to unstake. If ``None``, unstake all. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: success: `True` if extrinsic was finalized or included in the block. Flag is `True` if any wallet was - unstaked. If we did not wait for finalization/inclusion, the response is `True`. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" - ) - - new_amounts: Sequence[Optional[Balance]] - if amounts is None: - new_amounts = [None] * len(hotkey_ss58s) - else: - new_amounts = [ - Balance(amount) if not isinstance(amount, Balance) else amount - for amount in (amounts or [None] * len(hotkey_ss58s)) - ] - if sum(amount.tao for amount in new_amounts if amount is not None) == 0: - return True - - # Unlock coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ..." - ): - block_hash = await subtensor.substrate.get_chain_head() - - old_balance_ = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ) - old_stakes_ = asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - h, wallet.coldkeypub.ss58_address, block_hash - ) - for h in hotkey_ss58s - ] - ) - hotkey_owners_ = asyncio.gather( - *[subtensor.get_hotkey_owner(h, block_hash) for h in hotkey_ss58s] - ) - - old_balance, old_stakes, hotkey_owners, threshold = await asyncio.gather( - old_balance_, - old_stakes_, - hotkey_owners_, - _get_threshold_amount(subtensor, block_hash), - ) - own_hotkeys = [ - wallet.coldkeypub.ss58_address == hotkey_owner - for hotkey_owner in hotkey_owners - ] - - successful_unstakes = 0 - for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate( - zip(hotkey_ss58s, new_amounts, old_stakes, own_hotkeys) - ): - # Covert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - err_console.print( - f":cross_mark: [red]Not enough stake[/red]:" - f" [green]{stake_on_uid}[/green] to unstake:" - f" [blue]{unstaking_balance}[/blue] from hotkey:" - f" [white]{wallet.hotkey_str}[/white]" - ) - continue - - # If nomination stake, check threshold. - if ( - not own_hotkey - and ( - await _check_threshold_amount( - subtensor=subtensor, - sb=(stake_on_uid - unstaking_balance), - block_hash=block_hash, - min_req_stake=threshold, - ) - )[0] - is False - ): - console.print( - ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"Do you want to unstake:\n" - f"[bold white]\tamount: {unstaking_balance}\n" - f"ss58: {hotkey_ss58}[/bold white ]?" - ): - continue - - with console.status( - f":satellite: Unstaking from chain: [white]{subtensor}[/white] ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_unstaked": unstaking_balance.rao, - }, - ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_query = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="TxRateLimit", - block_hash=block_hash, - ) - tx_rate_limit_blocks: int = tx_query - - # TODO: Handle in-case we have fast blocks - if tx_rate_limit_blocks > 0: - console.print( - ":hourglass: [yellow]Waiting for tx rate limit:" - f" [white]{tx_rate_limit_blocks}[/white] blocks," - f" estimated time: [white]{tx_rate_limit_blocks * 12} [/white] seconds[/yellow]" - ) - await asyncio.sleep( - tx_rate_limit_blocks * 12 - ) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - successful_unstakes += 1 - continue - - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with console.status( - f":satellite: Checking stake balance on: [white]{subtensor}[/white] ..." - ): - new_stake = await subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block_hash=(await subtensor.substrate.get_chain_head()), - ) - console.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, stake_on_uid, new_stake - ) - ) - successful_unstakes += 1 - else: - err_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.") - continue - - if successful_unstakes != 0: - with console.status( - f":satellite: Checking balance on: ([white]{subtensor}[/white] ..." - ): - new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - console.print( - f"Balance: [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue]" - f" :arrow_right: [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" - ) - return True - - return False - - -# Commands async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", @@ -931,8 +139,8 @@ async def stake_add( starting_chain_head = await subtensor.substrate.get_chain_head() _all_dynamic_info, stake_info_dict = await asyncio.gather( - subtensor.get_all_subnet_dynamic_info(), - subtensor.get_stake_info_for_coldkeys( + subtensor.all_subnets(), + subtensor.get_stake_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], block_hash=starting_chain_head, ), @@ -998,15 +206,10 @@ async def stake_add( remaining_wallet_balance -= amount_to_stake_as_balance # Slippage warning - received_amount, slippage = dynamic_info.tao_to_alpha_with_slippage( - amount_to_stake_as_balance + received_amount, _, slippage_pct_float = ( + dynamic_info.tao_to_alpha_with_slippage(amount_to_stake_as_balance) ) if dynamic_info.is_dynamic: - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + received_amount) - if slippage + received_amount != 0 - else 0 - ) slippage_pct = f"{slippage_pct_float:.4f} %" rate = str(1 / (float(dynamic_info.price) or 1)) else: @@ -1114,7 +317,7 @@ async def send_extrinsic( else: new_balance_, stake_info_dict = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_info_for_coldkeys( + subtensor.get_stake_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], ), ) @@ -1179,7 +382,7 @@ async def unstake_selection( old_identities, netuid: Optional[int] = None, ): - stake_infos = await subtensor.get_stake_info_for_coldkey( + stake_infos = await subtensor.get_stake_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address ) @@ -1407,10 +610,10 @@ async def _unstake_all( all_sn_dynamic_info_, current_wallet_balance, ) = await asyncio.gather( - subtensor.get_stake_info_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address), subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), - subtensor.get_all_subnet_dynamic_info(), + subtensor.all_subnets(), subtensor.get_balance(wallet.coldkeypub.ss58_address), ) @@ -1472,8 +675,8 @@ async def _unstake_all( dynamic_info = all_sn_dynamic_info.get(stake.netuid) stake_amount = stake.stake - received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( - stake_amount + received_amount, _, slippage_pct_float = ( + dynamic_info.alpha_to_tao_with_slippage(stake_amount) ) total_received_value += received_amount @@ -1491,11 +694,6 @@ async def _unstake_all( hotkey_display = stake.hotkey_ss58 if dynamic_info.is_dynamic: - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + received_amount) - if slippage + received_amount != 0 - else 0 - ) slippage_pct = f"{slippage_pct_float:.4f} %" else: slippage_pct_float = 0 @@ -1600,7 +798,7 @@ async def unstake( spinner="earth", ): all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( - subtensor.get_all_subnet_dynamic_info(), + subtensor.all_subnets(), subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), ) @@ -1684,7 +882,7 @@ async def unstake( # Fetch stake balances chain_head = await subtensor.substrate.get_chain_head() - stake_info_dict = await subtensor.get_stake_info_for_coldkeys( + stake_info_dict = await subtensor.get_stake_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], block_hash=chain_head, ) @@ -1765,16 +963,11 @@ async def unstake( ) continue # Skip to the next subnet - useful when single amount is specified for all subnets - received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( - amount_to_unstake_as_balance + received_amount, _, slippage_pct_float = ( + dynamic_info.alpha_to_tao_with_slippage(amount_to_unstake_as_balance) ) total_received_amount += received_amount if dynamic_info.is_dynamic: - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + received_amount) - if slippage + received_amount != 0 - else 0 - ) slippage_pct = f"{slippage_pct_float:.4f} %" else: slippage_pct_float = 0 @@ -1961,7 +1154,7 @@ async def unstake( wallet.coldkeypub.ss58_address ) new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake_info = await subtensor.get_stake_info_for_coldkeys( + new_stake_info = await subtensor.get_stake_for_coldkeys( coldkey_ss58_list=[wallet.coldkeypub.ss58_address], ) new_stake = Balance.from_rao(0) @@ -2001,11 +1194,11 @@ async def get_stake_data(block_hash: str = None): registered_delegate_info, _dynamic_info, ) = await asyncio.gather( - subtensor.get_stake_info_for_coldkeys( + subtensor.get_stake_for_coldkeys( coldkey_ss58_list=[coldkey_address], block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.get_all_subnet_dynamic_info(), + subtensor.all_subnets(), ) sub_stakes = substakes[coldkey_address] dynamic_info = {info.netuid: info for info in _dynamic_info} @@ -2131,18 +1324,13 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): total_tao_value += tao_value # Swapped TAO value and slippage cell - swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( - substake_.stake + swapped_tao_value, _, slippage_percentage_ = ( + pool.alpha_to_tao_with_slippage(substake_.stake) ) total_swapped_tao_value += swapped_tao_value # Slippage percentage cell if pool.is_dynamic: - slippage_percentage_ = ( - 100 * float(slippage) / float(slippage + swapped_tao_value) - if slippage + swapped_tao_value != 0 - else 0 - ) slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" else: slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" @@ -2648,305 +1836,3 @@ def format_cell( for field_name, description in fields: description_table.add_row(field_name, description) console.print(description_table) - - -async def move_stake( - subtensor: "SubtensorInterface", - wallet: Wallet, - origin_netuid: int, - destination_netuid: int, - destination_hotkey: str, - amount: float, - stake_all: bool, - prompt: bool = True, -): - origin_hotkey_ss58 = wallet.hotkey.ss58_address - # Get the wallet stake balances. - origin_stake_balance = Balance.from_rao(0) - destination_stake_balance = Balance.from_rao(0) - - chain_head = await subtensor.substrate.get_chain_head() - stake_info_dict = await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], - block_hash=chain_head, - ) - - for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: - if ( - stake_info.hotkey_ss58 == origin_hotkey_ss58 - and stake_info.netuid == origin_netuid - ): - origin_stake_balance = stake_info.stake - elif ( - stake_info.hotkey_ss58 == destination_hotkey - and stake_info.netuid == destination_netuid - ): - destination_stake_balance = stake_info.stake - - # Set appropriate units - origin_stake_balance = origin_stake_balance.set_unit(origin_netuid) - destination_stake_balance = destination_stake_balance.set_unit(destination_netuid) - - if origin_stake_balance == Balance.from_tao(0).set_unit(origin_netuid): - print_error( - f"Your balance is [{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] in Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}" - ) - raise typer.Exit() - - console.print( - f"\nOrigin Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Origin stake: [{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}" - ) - console.print( - f"Destination netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Destination stake: [{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}\n" - ) - - # Determine the amount we are moving. - amount_to_move_as_balance = None - if amount: - amount_to_move_as_balance = Balance.from_tao(amount) - elif stake_all: - amount_to_move_as_balance = origin_stake_balance - else: # max_stake - # TODO improve this - if Confirm.ask( - f"Move all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]?" - ): - amount_to_move_as_balance = origin_stake_balance - else: - try: - amount = float( - Prompt.ask( - f"Enter amount to move in [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}" - ) - ) - amount_to_move_as_balance = Balance.from_tao(amount) - except ValueError: - print_error(f":cross_mark: Invalid amount: {amount}") - return False - - # Check enough to move. - amount_to_move_as_balance.set_unit(origin_netuid) - if amount_to_move_as_balance > origin_stake_balance: - err_console.print( - f"[red]Not enough stake[/red]:\n Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - ) - return False - - # Slippage warning - if prompt: - if origin_netuid == destination_netuid: - received_amount_destination = amount_to_move_as_balance - slippage_pct_float = 0 - slippage_pct = f"{slippage_pct_float}%" - price = Balance.from_tao(1).set_unit(origin_netuid) - price_str = ( - str(float(price.tao)) - + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}" - ) - else: - dynamic_origin, dynamic_destination = await asyncio.gather( - subtensor.get_subnet_dynamic_info(origin_netuid), - subtensor.get_subnet_dynamic_info(destination_netuid), - ) - price = ( - float(dynamic_origin.price) - * 1 - / (float(dynamic_destination.price) or 1) - ) - received_amount_tao, slippage = dynamic_origin.alpha_to_tao_with_slippage( - amount_to_move_as_balance - ) - received_amount_destination, slippage = ( - dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) - ) - received_amount_destination.set_unit(destination_netuid) - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + received_amount_destination) - if slippage + received_amount_destination != 0 - else 0 - ) - slippage_pct = f"{slippage_pct_float:.4f} %" - price_str = ( - str(float(price)) - + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" - ) - - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Moving stake from: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column( - "origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] - ) - table.add_column( - "origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - "dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] - ) - table.add_column( - "dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"amount ({Balance.get_unit(origin_netuid)})", - justify="center", - style=COLOR_PALETTE["STAKE"]["TAO"], - ) - table.add_column( - f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - f"received ({Balance.get_unit(destination_netuid)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - ) - table.add_column( - "slippage", - justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - ) - - table.add_row( - f"{Balance.get_unit(origin_netuid)}({origin_netuid})", - f"{origin_hotkey_ss58[:3]}...{origin_hotkey_ss58[-3:]}", - # TODO f-strings - Balance.get_unit(destination_netuid) + "(" + str(destination_netuid) + ")", - f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}", - str(amount_to_move_as_balance), - price_str, - str(received_amount_destination.set_unit(destination_netuid)), - str(slippage_pct), - ) - - console.print(table) - message = "" - if slippage_pct_float > 5: - message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - if not Confirm.ask("Would you like to continue?"): - return True - - # Perform staking operation. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - with console.status( - f"\n:satellite: Moving {amount_to_move_as_balance} from {origin_hotkey_ss58} on netuid: {origin_netuid} to " - f"{destination_hotkey} on netuid: {destination_netuid} ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="move_stake", - call_params={ - "origin_hotkey": origin_hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_hotkey": destination_hotkey, - "destination_netuid": destination_netuid, - "alpha_amount": amount_to_move_as_balance.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True - else: - await response.process_events() - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error:" - f" {format_error_message(response.error_message, subtensor.substrate)}" - ) - return - else: - new_stake_info_dict = await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], - ) - - new_origin_stake_balance = Balance.from_rao(0) - new_destination_stake_balance = Balance.from_rao(0) - - for stake_info in new_stake_info_dict[wallet.coldkeypub.ss58_address]: - if ( - stake_info.hotkey_ss58 == origin_hotkey_ss58 - and stake_info.netuid == origin_netuid - ): - new_origin_stake_balance = stake_info.stake.set_unit( - origin_netuid - ) - elif ( - stake_info.hotkey_ss58 == destination_hotkey - and stake_info.netuid == destination_netuid - ): - new_destination_stake_balance = stake_info.stake.set_unit( - destination_netuid - ) - - console.print( - f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" - ) - console.print( - f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" - ) - return - - -async def fetch_coldkey_stake(subtensor: "SubtensorInterface", wallet: Wallet): - sub_stakes = await subtensor.get_stake_info_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - return sub_stakes - - -# TODO: Use this in all subnet commands. -async def get_stake_info_for_coldkey_and_hotkey( - subtensor: "SubtensorInterface", - coldkey_ss58: str, - hotkey_ss58: Optional[str] = None, - netuid: Optional[int] = None, - block_hash: Optional[str] = None, -) -> dict[tuple[str, int], Balance]: - """Helper function to get stake info for a coldkey and optionally filter by hotkey and netuid. - - Args: - subtensor: SubtensorInterface instance - coldkey_ss58: Coldkey SS58 address - hotkey_ss58: Optional hotkey SS58 address to filter by - netuid: Optional netuid to filter by - block_hash: Optional block hash to query at - - Returns: - Dictionary mapping (hotkey, netuid) tuple to stake balance - """ - stake_info_dict = await subtensor.get_stake_info_for_coldkeys( - coldkey_ss58_list=[coldkey_ss58], block_hash=block_hash - ) - - stakes = {} - for stake_info in stake_info_dict[coldkey_ss58]: - if hotkey_ss58 and stake_info.hotkey_ss58 != hotkey_ss58: - continue - if netuid is not None and stake_info.netuid != netuid: - continue - stakes[(stake_info.hotkey_ss58, stake_info.netuid)] = stake_info.stake - - return stakes diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 5783640a..f1d71d64 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -1,5 +1,6 @@ import asyncio import json +import math from pywry import PyWry from typing import TYPE_CHECKING @@ -24,6 +25,7 @@ async def price( all_netuids: bool = False, interval_hours: int = 24, html_output: bool = False, + log_scale: bool = False, ): """ Fetch historical price data for subnets and display it in a chart. @@ -48,17 +50,13 @@ async def price( ] block_hashes = await asyncio.gather(*block_hash_cors) - # Subnet info (we fetch all subnets in-case there are more than one) + # We fetch all subnets when there is more than one netuid if all_netuids or len(netuids) > 1: - subnet_info_cors = [ - subtensor.get_all_subnet_dynamic_info(bh) for bh in block_hashes - ] + subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes] else: # If there is only one netuid, we fetch the subnet info for that netuid netuid = netuids[0] - subnet_info_cors = [ - subtensor.get_subnet_dynamic_info(netuid, bh) for bh in block_hashes - ] + subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes] all_subnet_infos = await asyncio.gather(*subnet_info_cors) subnet_data = _process_subnet_data( @@ -70,9 +68,11 @@ async def price( return if html_output: - await _generate_html_output(subnet_data, block_numbers, interval_hours) + await _generate_html_output( + subnet_data, block_numbers, interval_hours, log_scale + ) else: - _generate_cli_output(subnet_data, block_numbers, interval_hours) + _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) def _process_subnet_data( @@ -86,7 +86,6 @@ def _process_subnet_data( Process subnet data into a structured format for price analysis. """ subnet_data = {} - if all_netuids or len(netuids) > 1: for netuid in netuids: prices = [] @@ -187,6 +186,7 @@ def _generate_html_single_subnet( data, block_numbers, interval_hours, + log_scale, ): """ Generate an HTML chart for a single subnet. @@ -203,7 +203,7 @@ def _generate_html_single_subnet( name=f"Subnet {netuid} - {stats['name']}" if stats["name"] else f"Subnet {netuid}", - line=dict(width=2, color="#1f77b4"), + line=dict(width=2, color="#50C878"), ) ) @@ -226,16 +226,22 @@ def _generate_html_single_subnet( height=600, ) + price_title = f"Price ({stats['symbol']})" + if log_scale: + price_title += " Log Scale" + # Label axes fig.update_xaxes( title="Block", gridcolor="rgba(128,128,128,0.2)", zerolinecolor="rgba(128,128,128,0.2)", + type="log" if log_scale else "linear", ) fig.update_yaxes( - title=f"Price ({stats['symbol']})", + title=price_title, gridcolor="rgba(128,128,128,0.2)", zerolinecolor="rgba(128,128,128,0.2)", + type="log" if log_scale else "linear", ) # Price change color @@ -350,7 +356,7 @@ def _generate_html_single_subnet( return html_content -def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours): +def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours, log_scale): """ Generate an HTML chart for multiple subnets. """ @@ -381,16 +387,22 @@ def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours): height=700, ) + price_title = "Price (τ)" + if log_scale: + price_title += " Log Scale" + # Label axes fig.update_xaxes( title="Block", gridcolor="rgba(128,128,128,0.2)", zerolinecolor="rgba(128,128,128,0.2)", + type="log" if log_scale else "linear", ) fig.update_yaxes( - title="Price (τ)", + title=price_title, gridcolor="rgba(128,128,128,0.2)", zerolinecolor="rgba(128,128,128,0.2)", + type="log" if log_scale else "linear", ) # Create annotation for top subnet @@ -573,12 +585,13 @@ def build_single_subnet_annotations(netuid): "annotations": json.dumps(mode_data["annotations"]), } - # Build buttons for each subnet + # We sort netuids by market cap but for buttons, they are ordered by netuid + sorted_subnet_keys = sorted(subnet_data.keys()) all_button_html = ( '' ) subnet_buttons_html = "" - for netuid in subnet_keys: + for netuid in sorted_subnet_keys: subnet_buttons_html += f' ' html_content = f""" @@ -706,6 +719,7 @@ async def _generate_html_output( subnet_data, block_numbers, interval_hours, + log_scale: bool = False, ): """ Start PyWry and display the price chart in a window. @@ -718,53 +732,41 @@ async def _generate_html_output( netuid = subnet_keys[0] data = subnet_data[netuid] html_content = _generate_html_single_subnet( - netuid, data, block_numbers, interval_hours - ) - console.print( - "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" - ) - - handler = PyWry() - handler.send_html( - html=html_content, - title=f"Subnet {netuid} Price View", - width=1200, - height=800, + netuid, data, block_numbers, interval_hours, log_scale ) - handler.start() - try: - while True: - await asyncio.sleep(1) - except KeyboardInterrupt: - handler.close() - + title = f"Subnet {netuid} Price View" else: # Multi-subnet - console.print( - "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" - ) html_content = _generate_html_multi_subnet( - subnet_data, block_numbers, interval_hours + subnet_data, block_numbers, interval_hours, log_scale ) - handler = PyWry() - handler.send_html( - html=html_content, - title="Multi-Subnet Price Chart", - width=1200, - height=800, - ) - handler.start() + title = "Subnets Price Chart" + + console.print( + "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" + ) + + handler = PyWry() + handler.send_html( + html=html_content, + title=title, + width=1200, + height=800, + ) + handler.start() + try: + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: try: - while True: - await asyncio.sleep(1) - except KeyboardInterrupt: handler.close() - + except (ProcessLookupError, Exception): + pass except Exception as e: - print_error(f"Error: {e}") + print_error(f"Error generating price chart: {e}") -def _generate_cli_output(subnet_data, block_numbers, interval_hours): +def _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale): """ Render the price data in a textual CLI style with plotille ASCII charts. """ @@ -779,12 +781,17 @@ def color_label(text): return plotille.color(text, fg=(186, 233, 143), mode="rgb") fig.x_label = color_label("Block") - fig.y_label = color_label(f"Price ({data['stats']['symbol']})") + y_label_text = f"Price ({data['stats']['symbol']})" + fig.y_label = color_label(y_label_text) + + prices = data["prices"] + if log_scale: + prices = [math.log10(p) for p in prices] fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers)) fig.set_y_limits( - min_=data["stats"]["low"] * 0.95, - max_=data["stats"]["high"] * 1.05, + min_=data["stats"]["low"] * 0.99, + max_=data["stats"]["high"] * 1.01, ) fig.plot( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 56b08739..a17e976f 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -204,7 +204,7 @@ async def subnets_list( async def fetch_subnet_data(): block_number = await subtensor.substrate.get_block_number(None) - subnets = await subtensor.get_all_subnet_dynamic_info() + subnets = await subtensor.all_subnets() # Sort subnets by market cap, keeping the root subnet in the first position root_subnet = next(s for s in subnets if s.netuid == 0) @@ -800,7 +800,7 @@ async def show( prompt: bool = True, ) -> Optional[str]: async def show_root(): - all_subnets = await subtensor.get_all_subnet_dynamic_info() + all_subnets = await subtensor.all_subnets() root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: print_error("The root subnet does not exist") @@ -1024,12 +1024,12 @@ async def show_subnet(netuid_: int): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") raise typer.Exit() ( - _subnet_info, + subnet_info, hex_bytes_result, identities, old_identities, ) = await asyncio.gather( - subtensor.get_all_subnet_dynamic_info(), + subtensor.subnet(netuid=netuid_), subtensor.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", @@ -1038,7 +1038,6 @@ async def show_subnet(netuid_: int): subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - subnet_info = next((s for s in _subnet_info if s.netuid == netuid_), None) if subnet_info is None: print_error(f"Subnet {netuid_} does not exist") raise typer.Exit() diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e7eb151d..19fb847d 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -497,8 +497,7 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): print_error(f"Subnet with netuid {netuid} does not exist.") return False subnet = await subtensor.get_subnet_hyperparameters(netuid) - _subnet_info = await subtensor.get_all_subnet_dynamic_info() - subnet_info = next((s for s in _subnet_info if s.netuid == netuid), None) + subnet_info = await subtensor.subnet(netuid) if subnet_info is None: print_error(f"Subnet with netuid {netuid} does not exist.") raise typer.Exit() diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 81270ee0..71d24df3 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2,7 +2,7 @@ import itertools import os from collections import defaultdict -from typing import Any, Collection, Generator, Optional +from typing import Any, Generator, Optional import aiohttp from bittensor_wallet import Wallet, Keypair @@ -23,7 +23,6 @@ from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, NeuronInfoLite, - StakeInfo, ) from bittensor_cli.src.bittensor.extrinsics.registration import ( run_faucet_extrinsic, @@ -610,7 +609,7 @@ async def overview( all_hotkeys, total_balance = await _get_total_balance( total_balance, subtensor, wallet, all_wallets, block_hash=block_hash ) - _dynamic_info = await subtensor.get_all_subnet_dynamic_info() + _dynamic_info = await subtensor.all_subnets() dynamic_info = {info.netuid: info for info in _dynamic_info} with console.status( @@ -1094,55 +1093,6 @@ async def _get_neurons_for_netuids( ] -async def _get_de_registered_stake_for_coldkey_wallet( - subtensor: SubtensorInterface, - all_hotkey_addresses: Collection[str], - coldkey_wallet: Wallet, -) -> tuple[Wallet, list[tuple[str, float]], Optional[str]]: - """ - Looks at the total stake of a coldkey, then filters this based on the supplied hotkey addresses - depending on whether the hotkey is a delegate - - :param subtensor: SubtensorInterface to make queries with - :param all_hotkey_addresses: collection of hotkey SS58 addresses - :param coldkey_wallet: Wallet containing coldkey - - :return: (original wallet, [(hotkey SS58, stake in TAO), ...], error message) - """ - # Pull all stake for our coldkey - all_stake_info_for_coldkey = await subtensor.get_stake_info_for_coldkey( - coldkey_ss58=coldkey_wallet.coldkeypub.ss58_address, reuse_block=True - ) - - # Filter out hotkeys that are in our wallets - # Filter out hotkeys that are delegates. - async def _filter_stake_info(stake_info: StakeInfo) -> bool: - if stake_info.stake == 0: - return False # Skip hotkeys that we have no stake with. - if stake_info.hotkey_ss58 in all_hotkey_addresses: - return False # Skip hotkeys that are in our wallets. - return not await subtensor.is_hotkey_delegate( - hotkey_ss58=stake_info.hotkey_ss58, reuse_block=True - ) - - all_staked = await asyncio.gather( - *[_filter_stake_info(stake_info) for stake_info in all_stake_info_for_coldkey] - ) - - # Collecting all filtered stake info using async for loop - all_staked_hotkeys = [] - for stake_info, staked in zip(all_stake_info_for_coldkey, all_staked): - if staked: - all_staked_hotkeys.append( - ( - stake_info.hotkey_ss58, - stake_info.stake.tao, - ) - ) - - return coldkey_wallet, all_staked_hotkeys, None - - async def transfer( wallet: Wallet, subtensor: SubtensorInterface, From f2b135e43ad001bd9420b10216fca234c7f1cef8 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 24 Jan 2025 18:29:55 -0800 Subject: [PATCH 228/332] Adds linux deps msg + cleaner shutdown of price --- bittensor_cli/cli.py | 5 ++++ bittensor_cli/src/bittensor/utils.py | 30 +++++++++++++++++---- bittensor_cli/src/commands/subnets/price.py | 27 ++++++++++++++----- bittensor_cli/src/commands/wallets.py | 14 +++++----- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2fa9ba5a..889b18fc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -48,6 +48,8 @@ prompt_for_identity, validate_uri, prompt_for_subnet_identity, + print_linux_dependency_message, + is_linux, ) from typing_extensions import Annotated from textwrap import dedent @@ -3944,6 +3946,9 @@ def subnets_price( if all_netuids: html_output = True + if html_output and is_linux(): + print_linux_dependency_message() + return self._run_command( price.price( self.initialize_chain(network), diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 869116fd..08cfd9e0 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -2,6 +2,7 @@ import math import os import sqlite3 +import platform import webbrowser import sys from pathlib import Path @@ -1202,17 +1203,36 @@ def is_valid_contact(contact: str) -> bool: def get_subnet_name(subnet_info) -> str: """Get the subnet name, prioritizing subnet_identity.subnet_name over subnet.subnet_name. - + Args: subnet: The subnet dynamic info - + Returns: str: The subnet name or empty string if no name is found """ return ( subnet_info.subnet_identity.subnet_name - if hasattr(subnet_info, 'subnet_identity') - and subnet_info.subnet_identity is not None + if hasattr(subnet_info, "subnet_identity") + and subnet_info.subnet_identity is not None and subnet_info.subnet_identity.subnet_name is not None else (subnet_info.subnet_name if subnet_info.subnet_name is not None else "") - ) \ No newline at end of file + ) + + +def print_linux_dependency_message(): + """Prints the WebKit dependency message for Linux systems.""" + console.print("[red]This command requires WebKit dependencies on Linux.[/red]") + console.print( + "\nPlease install the required packages using one of the following commands based on your distribution:" + ) + console.print("\nArch Linux / Manjaro:") + console.print("[green]sudo pacman -S webkit2gtk[/green]") + console.print("\nDebian / Ubuntu:") + console.print("[green]sudo apt install libwebkit2gtk-4.0-dev[/green]") + console.print("\nFedora / CentOS / AlmaLinux:") + console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]") + + +def is_linux(): + """Returns True if the operating system is Linux.""" + return platform.system().lower() == "linux" diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index f1d71d64..eb6b7b5c 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -741,11 +741,9 @@ async def _generate_html_output( subnet_data, block_numbers, interval_hours, log_scale ) title = "Subnets Price Chart" - console.print( "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]" ) - handler = PyWry() handler.send_html( html=html_content, @@ -754,14 +752,22 @@ async def _generate_html_output( height=800, ) handler.start() + await asyncio.sleep(5) + + # TODO: Improve this logic try: while True: + if _has_exited(handler): + break await asyncio.sleep(1) except KeyboardInterrupt: - try: - handler.close() - except (ProcessLookupError, Exception): - pass + pass + finally: + if not _has_exited(handler): + try: + handler.close() + except Exception: + pass except Exception as e: print_error(f"Error generating price chart: {e}") @@ -850,3 +856,12 @@ def color_label(text): ) console.print(stats_text) + + +def _has_exited(handler) -> bool: + """Check if PyWry process has cleanly exited with returncode 0.""" + return ( + hasattr(handler, "runner") + and handler.runner is not None + and handler.runner.returncode == 0 + ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 71d24df3..c2b8e021 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -149,7 +149,7 @@ async def new_hotkey( use_password=use_password, overwrite=False, ) - console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") + console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -178,7 +178,7 @@ async def new_coldkey( use_password=use_password, overwrite=False, ) - console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") + console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -193,11 +193,11 @@ async def wallet_create( if uri: try: keypair = Keypair.create_from_uri(uri) + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) except Exception as e: print_error(f"Failed to create keypair from URI: {str(e)}") - wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) - wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) console.print( f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]" ) @@ -208,7 +208,7 @@ async def wallet_create( use_password=use_password, overwrite=False, ) - console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") + console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -218,7 +218,7 @@ async def wallet_create( use_password=False, overwrite=False, ) - console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") + console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") From 609f6a0c013d947aa6cc09523712cb77ad7908c9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 24 Jan 2025 18:34:56 -0800 Subject: [PATCH 229/332] Bumps versions + requirements --- CHANGELOG.md | 13 +++++++++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a879b33..de809acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 8.2.0rc14 /2025-01-24 + +## What's Changed +* Adds stake_swap, stake_transfer. +* Updates stake_move with interactive mode +* Clean up of outdated subtensor methods + code +* Adds --html option in price command + +## 8.2.0rc13 /2025-01-15 + +## What's Changed +* Bux fixes + ## 8.2.0rc12 /2025-01-14 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index d0da2734..8ecf1c97 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc13" +__version__ = "8.2.0rc14" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 889b18fc..f5924cee 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -64,7 +64,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc13" +__version__ = "8.2.0rc14" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 0af6ec08cb88f14cbbfaf3949d4051d1368d1f99 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 27 Jan 2025 11:38:32 -0800 Subject: [PATCH 230/332] Adds subnet state method --- .../src/bittensor/subtensor_interface.py | 31 ++++++++++ bittensor_cli/src/commands/subnets/subnets.py | 58 +++++-------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d017f151..a5f79776 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -27,6 +27,7 @@ decode_hex_identity, DelegateInfoLite, DynamicInfo, + SubnetState, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, FixedPoint, fixed_to_float @@ -588,6 +589,36 @@ async def subnet_exists( ) return result + async def get_subnet_state( + self, netuid: int, block_hash: Optional[str] = None + ) -> Optional["SubnetState"]: + """ + Retrieves the state of a specific subnet within the Bittensor network. + + Args: + netuid: The network UID of the subnet to query. + block_hash: The hash of the blockchain block number for the query. + + Returns: + SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. + """ + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid], + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return None + + try: + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + except ValueError: + bytes_result = bytes.fromhex(hex_bytes_result) + + return SubnetState.from_vec_u8(bytes_result) + async def get_hyperparameter( self, param_name: str, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index a17e976f..88fb63ec 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -806,24 +806,16 @@ async def show_root(): print_error("The root subnet does not exist") raise typer.Exit() - hex_bytes_result, identities, old_identities = await asyncio.gather( - subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_state", - params=[0], - ), + root_state, identities, old_identities = await asyncio.gather( + subtensor.get_subnet_state(netuid=0), subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - if (bytes_result := hex_bytes_result) is None: + if root_state is None: err_console.print("The root subnet does not exist") return - if bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(bytes_result[2:]) - - root_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) if len(root_state.hotkeys) == 0: err_console.print( "The root-subnet is currently empty with 0 UIDs registered." @@ -1025,35 +1017,24 @@ async def show_subnet(netuid_: int): raise typer.Exit() ( subnet_info, - hex_bytes_result, + subnet_state, identities, old_identities, ) = await asyncio.gather( subtensor.subnet(netuid=netuid_), - subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_state", - params=[netuid_], - ), + subtensor.get_subnet_state(netuid=netuid_), subtensor.query_all_identities(), subtensor.get_delegate_identities(), ) - if subnet_info is None: - print_error(f"Subnet {netuid_} does not exist") - raise typer.Exit() - - if (bytes_result := hex_bytes_result) is None: + if subnet_state is None: print_error(f"Subnet {netuid_} does not exist") raise typer.Exit() - if bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(bytes_result[2:]) - - subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) if subnet_info is None: print_error(f"Subnet {netuid_} does not exist") raise typer.Exit() - elif len(subnet_state.hotkeys) == 0: + + if len(subnet_state.hotkeys) == 0: print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.") raise typer.Exit() @@ -1585,7 +1566,13 @@ async def metagraph_cmd( print_error(f"Subnet with netuid: {netuid} does not exist", status) return False - neurons, difficulty_, total_issuance_, block = await asyncio.gather( + ( + neurons, + difficulty_, + total_issuance_, + block, + subnet_state, + ) = await asyncio.gather( subtensor.neurons(netuid, block_hash=block_hash), subtensor.get_hyperparameter( param_name="Difficulty", netuid=netuid, block_hash=block_hash @@ -1597,22 +1584,9 @@ async def metagraph_cmd( block_hash=block_hash, ), subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_subnet_state(netuid=netuid), ) - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_state", - params=[netuid], - ) - if not (bytes_result := hex_bytes_result): - err_console.print(f"Subnet {netuid} does not exist") - return - - if bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(bytes_result[2:]) - - subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) - difficulty = int(difficulty_) total_issuance = Balance.from_rao(total_issuance_) metagraph = MiniGraph( From 01cc17772dca3a367053a6fe3647b2430e07486e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 27 Jan 2025 13:42:15 -0800 Subject: [PATCH 231/332] Adds validation for hotkey --- bittensor_cli/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f5924cee..7bd37fbb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4041,8 +4041,10 @@ def subnets_create( wallet_hotkey, ask_for=[ WO.NAME, + WO.HOTKEY, + WO.PATH, ], - validate=WV.WALLET, + validate=WV.WALLET_AND_HOTKEY, ) identity = prompt_for_subnet_identity( subnet_name=subnet_name, From 74323c184c53b59a394a0a64ff33439266b95b25 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 28 Jan 2025 01:11:07 -0800 Subject: [PATCH 232/332] Updates Rao to decode using chain --- bittensor_cli/src/__init__.py | 197 --- .../bittensor/async_substrate_interface.py | 262 +++- bittensor_cli/src/bittensor/chain_data.py | 1319 ++++------------- .../src/bittensor/extrinsics/registration.py | 25 +- .../src/bittensor/extrinsics/transfer.py | 15 +- .../src/bittensor/subtensor_interface.py | 202 +-- bittensor_cli/src/bittensor/utils.py | 63 +- bittensor_cli/src/commands/stake/stake.py | 74 +- bittensor_cli/src/commands/subnets/subnets.py | 31 +- bittensor_cli/src/commands/wallets.py | 24 +- requirements.txt | 2 +- 11 files changed, 683 insertions(+), 1531 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index ffd5ab26..d775b546 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -143,203 +143,6 @@ class WalletValidationTypes(Enum): "types": { "Balance": "u64", # Need to override default u128 }, - "runtime_api": { - "DelegateInfoRuntimeApi": { - "methods": { - "get_delegated": { - "params": [ - { - "name": "coldkey", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_delegates": { - "params": [], - "type": "Vec", - }, - } - }, - "NeuronInfoRuntimeApi": { - "methods": { - "get_neuron_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - { - "name": "uid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neurons_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neuron": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - { - "name": "uid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neurons": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - } - }, - "StakeInfoRuntimeApi": { - "methods": { - "get_stake_info_for_coldkey": { - "params": [{"name": "coldkey_account_vec", "type": "Vec"}], - "type": "Vec", - }, - "get_stake_info_for_coldkeys": { - "params": [ - {"name": "coldkey_account_vecs", "type": "Vec>"} - ], - "type": "Vec", - }, - "get_subnet_stake_info_for_coldkeys": { - "params": [ - {"name": "coldkey_account_vecs", "type": "Vec>"}, - {"name": "netuid", "type": "u16"}, - ], - "type": "Vec", - }, - "get_subnet_stake_info_for_coldkey": { - "params": [ - {"name": "coldkey_account_vec", "type": "Vec"}, - {"name": "netuid", "type": "u16"}, - ], - "type": "Vec", - }, - "get_total_subnet_stake": { - "params": [{"name": "netuid", "type": "u16"}], - "type": "Vec", - }, - } - }, - "ValidatorIPRuntimeApi": { - "methods": { - "get_associated_validator_ip_info_for_subnet": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - }, - }, - "SubnetInfoRuntimeApi": { - "methods": { - "get_subnet_hyperparams": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_subnet_info": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_subnets_info": { - "params": [], - "type": "Vec", - }, - "get_subnet_info_v2": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_subnets_info_v2": { - "params": [], - "type": "Vec", - }, - "get_all_dynamic_info": { - "params": [], - "type": "Vec", - }, - "get_dynamic_info": { - "params": [{"name": "netuid", "type": "u16"}], - "type": "Vec", - }, - "get_subnet_state": { - "params": [{"name": "netuid", "type": "u16"}], - "type": "Vec", - }, - } - }, - "SubnetRegistrationRuntimeApi": { - "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} - }, - "ColdkeySwapRuntimeApi": { - "methods": { - "get_scheduled_coldkey_swap": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_remaining_arbitration_period": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_coldkey_swap_destinations": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - } - }, - }, } UNITS = [ diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 342c2c78..e9419c07 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -4,9 +4,18 @@ from collections import defaultdict from dataclasses import dataclass from hashlib import blake2b +from itertools import chain from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING +from types import SimpleNamespace + +from bt_decode import ( + PortableRegistry, + decode as decode_by_type_string, + encode as encode_by_type_string, + MetadataV15, +) + -from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 from async_property import async_property from scalecodec import GenericExtrinsic from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject @@ -22,6 +31,12 @@ from websockets.asyncio.client import connect from websockets.exceptions import ConnectionClosed +from .utils import ( + bytes_from_hex_string_result, + encode_account_id, + decode_account_id, +) + if TYPE_CHECKING: from websockets.asyncio.client import ClientConnection @@ -36,6 +51,33 @@ def timeout_handler(signum, frame): raise TimeoutException("Operation timed out") +class DictWithValue(dict): + value: Any + + def __init__(self, value: Any = None): + super().__init__() + self.value = value + + def __getitem__(self, key: Union[str, int]): + result = super().get(key) + if not result and isinstance(key, int): + # if the key is not found, return the key at the given index + return list(chain.from_iterable(self.items()))[key] + return result + + @classmethod + def from_dict(cls, dict_: dict): + inst = cls() + # recursively convert all values to DictWithValue + for key, value in dict_.items(): + if isinstance(value, dict): + value = cls.from_dict(value) + inst[key] = value + inst.value = dict_ + + return inst + + class ExtrinsicReceipt: """ Object containing information of submitted extrinsic. Block hash where extrinsic is included is required @@ -754,6 +796,7 @@ async def retrieve(self, item_id: int) -> Optional[dict]: class AsyncSubstrateInterface: runtime = None registry: Optional[PortableRegistry] = None + metadata_v15: Optional[MetadataV15] = None def __init__( self, @@ -799,6 +842,7 @@ def __init__( self.transaction_version = None self.metadata = None self.metadata_version_hex = "0x0f000000" # v15 + self.metadata_v15 = None async def __aenter__(self): await self.initialize() @@ -819,6 +863,64 @@ async def initialize(self): async def __aexit__(self, exc_type, exc_val, exc_tb): pass + @staticmethod + def _type_registry_to_scale_info_types( + registry_types: list[dict[str, Any]], + ) -> list[dict[str, Any]]: + scale_info_types = [] + for type_entry in registry_types: + new_type_entry = DictWithValue(value=type_entry) + if ( + "variant" in type_entry["type"]["def"] + and len(type_entry["type"]["def"]["variant"]) == 0 + ): + type_entry["type"]["def"]["variant"] = { + "variants": [] + } # add empty variants field to variant type if empty + + for key, value in type_entry.items(): + if isinstance(value, dict): + entry = DictWithValue.from_dict(value) + else: + entry = SimpleNamespace(value=value) + new_type_entry[key] = entry + + scale_info_types.append(new_type_entry) + + return scale_info_types + + @staticmethod + def _type_id_to_name(ty_id: int) -> str: + type_string = f"scale_info::{ty_id}" + + return type_string + + def _type_registry_apis_to_runtime_api( + self, apis: list[dict[str, Any]] + ) -> dict[str, Any]: + runtime_api = {} + for api in apis: + api_name = api["name"] + methods = api["methods"] + + runtime_api[api_name] = { + "methods": { + method["name"]: { + "description": "\n".join(method["docs"]), + "params": [ + { + "name": input["name"], + "type": self._type_id_to_name(input["ty"]), + } + for input in method["inputs"] + ], + "type": self._type_id_to_name(method["output"]), + } + for method in methods + } + } + return runtime_api + @property def chain(self): """ @@ -853,6 +955,7 @@ async def load_registry(self): metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:]) metadata_v15 = MetadataV15.decode_from_metadata_option(metadata_option_bytes) self.registry = PortableRegistry.from_metadata_v15(metadata_v15) + self.metadata_v15 = metadata_v15 async def decode_scale( self, type_string, scale_bytes: bytes, return_scale_obj=False @@ -875,10 +978,65 @@ async def decode_scale( """ if scale_bytes == b"\x00": obj = None + if type_string == "scale_info::0": # Is an AccountId + # Decode AccountId bytes to SS58 address + return decode_account_id(scale_bytes) else: obj = decode_by_type_string(type_string, self.registry, scale_bytes) return obj + async def encode_scale(self, type_string, value: Any) -> bytes: + """ + Helper function to encode arbitrary objects according to given RUST type_string + (e.g. BlockNumber). + + Parameters + ---------- + type_string + value + + Returns + ------- + bytes: encoded SCALE bytes + + """ + if value is None: + result = b"\x00" + else: + if type_string == "scale_info::0": # Is an AccountId + # encode string into AccountId + ## AccountId is a composite type with one, unnamed field + return encode_account_id(value) + + elif type_string == "scale_info::151": # Vec + if not isinstance(value, (list, tuple)): + value = [value] + + # Encode length + length = len(value) + if length < 64: + result = bytes([length << 2]) # Single byte mode + else: + raise ValueError("Vector length too large") + + # Encode each AccountId + for account in value: + if isinstance(account, bytes): + result += account # Already encoded + else: + result += encode_account_id(account) # SS58 string + return result + + if isinstance(value, ScaleType): + if value.data.data is not None: + # Already encoded + return bytes(value.data.data) + else: + value = value.value # Unwrap the value of the type + + result = bytes(encode_by_type_string(type_string, self.registry, value)) + return result + async def init_runtime( self, block_hash: Optional[str] = None, block_id: Optional[int] = None ) -> Runtime: @@ -1872,16 +2030,16 @@ async def query_multi( storage_key = storage_key_map[change_storage_key] if change_data is None: change_data = b"\x00" + obj = None else: change_data = bytes.fromhex(change_data[2:]) - result.append( - ( - storage_key, - await self.decode_scale( + if change_data == b"\x00": + obj = None + else: + obj = await self.decode_scale( storage_key.value_scale_type, change_data - ), - ) - ) + ) + result.append((storage_key, obj)) return result @@ -2177,13 +2335,13 @@ async def get_chain_finalised_head(self): return response.get("result") - async def runtime_call( + async def runtime_call_wait_to_decode( self, api: str, method: str, params: Optional[Union[list, dict]] = None, block_hash: Optional[str] = None, - ) -> ScaleType: + ) -> tuple[str, bytes]: """ Calls a runtime API method @@ -2192,7 +2350,7 @@ async def runtime_call( :param params: List of parameters needed to call the runtime API :param block_hash: Hash of the block at which to make the runtime API call - :return: ScaleType from the runtime call + :return: Tuple of the runtime call type and the result bytes """ await self.init_runtime() @@ -2200,58 +2358,60 @@ async def runtime_call( params = {} try: - runtime_call_def = self.runtime_config.type_registry["runtime_api"][api][ - "methods" - ][method] - runtime_api_types = self.runtime_config.type_registry["runtime_api"][ - api - ].get("types", {}) + metadata_v15 = self.metadata_v15.value() + apis = {entry["name"]: entry for entry in metadata_v15["apis"]} + api_entry = apis[api] + methods = {entry["name"]: entry for entry in api_entry["methods"]} + runtime_call_def = methods[method] except KeyError: raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") - if isinstance(params, list) and len(params) != len(runtime_call_def["params"]): + if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]): raise ValueError( f"Number of parameter provided ({len(params)}) does not " - f"match definition {len(runtime_call_def['params'])}" + f"match definition {len(runtime_call_def['inputs'])}" ) - # Add runtime API types to registry - self.runtime_config.update_type_registry_types(runtime_api_types) - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - # Encode params - param_data = ScaleBytes(bytes()) - for idx, param in enumerate(runtime_call_def["params"]): - scale_obj = runtime.runtime_config.create_scale_object(param["type"]) + param_data = b"" + for idx, param in enumerate(runtime_call_def["inputs"]): + param_type_string = f'scale_info::{param["ty"]}' if isinstance(params, list): - param_data += scale_obj.encode(params[idx]) + param_data += await self.encode_scale(param_type_string, params[idx]) else: if param["name"] not in params: raise ValueError(f"Runtime Call param '{param['name']}' is missing") - param_data += scale_obj.encode(params[param["name"]]) + param_data += await self.encode_scale( + param_type_string, params[param["name"]] + ) # RPC request result_data = await self.rpc_request( - "state_call", [f"{api}_{method}", str(param_data), block_hash] + "state_call", [f"{api}_{method}", param_data.hex(), block_hash] ) + output_type_string = f'scale_info::{runtime_call_def["output"]}' - # Decode result - # TODO update this to use bt-decode - result_obj = runtime.runtime_config.create_scale_object( - runtime_call_def["type"] - ) - result_obj.decode( - ScaleBytes(result_data["result"]), - check_remaining=self.config.get("strict_scale_decode"), + return output_type_string, bytes_from_hex_string_result(result_data["result"]) + + async def runtime_call( + self, + api: str, + method: str, + params: Optional[Union[list, dict]] = None, + block_hash: Optional[str] = None, + ) -> Any: + """ + Calls a runtime API method and decodes the result. + """ + # Get the runtime call type and result bytes + runtime_call_type, result_bytes = await self.runtime_call_wait_to_decode( + api, method, params, block_hash ) - return result_obj + # Decode using the type + result = await self.decode_scale(runtime_call_type, result_bytes) + return result async def get_account_nonce(self, account_address: str) -> int: """ @@ -2264,7 +2424,7 @@ async def get_account_nonce(self, account_address: str) -> int: nonce_obj = await self.runtime_call( "AccountNonceApi", "account_nonce", [account_address] ) - return nonce_obj.value + return nonce_obj async def get_metadata_constant(self, module_name, constant_name, block_hash=None): """ @@ -2357,14 +2517,16 @@ async def get_payment_info( extrinsic = await self.create_signed_extrinsic( call=call, keypair=keypair, signature=signature ) - extrinsic_len = self.runtime_config.create_scale_object("u32") - extrinsic_len.encode(len(extrinsic.data)) - - result = await self.runtime_call( - "TransactionPaymentApi", "query_info", [extrinsic, extrinsic_len] + extrinsic_len = len(extrinsic.data) + result = ( + await self.runtime_call( # Needs the call hex, not the extrinsic object + "TransactionPaymentApi", + "query_info", + [extrinsic, extrinsic_len], + ) ) - return result.value + return result async def query( self, diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 97949867..9a65324a 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,82 +1,32 @@ +from abc import abstractmethod from dataclasses import dataclass from enum import Enum from typing import Optional, Any, Union -import bt_decode import netaddr -from scalecodec import ScaleBytes -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.networking import int_to_ip -from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float +from bittensor_cli.src.bittensor.utils import ( + SS58_FORMAT, + u16_normalized_float, + decode_account_id, +) class ChainDataType(Enum): NeuronInfo = 1 - SubnetInfoV2 = 2 - DelegateInfo = 3 - NeuronInfoLite = 4 - DelegatedInfo = 5 - StakeInfo = 6 - IPInfo = 7 - SubnetHyperparameters = 8 - SubstakeElements = 9 - DynamicPoolInfoV2 = 10 - DelegateInfoLite = 11 - DynamicInfo = 12 - ScheduledColdkeySwapInfo = 13 - SubnetInfo = 14 - SubnetState = 15 - SubnetIdentity = 16 - - -def from_scale_encoding_using_type_string( - input_: Union[list[int], bytes, ScaleBytes], type_string: str -) -> Optional[dict]: - if isinstance(input_, ScaleBytes): - as_scale_bytes = input_ - else: - if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): - vec_u8 = input_ - as_bytes = bytes(vec_u8) - elif isinstance(input_, bytes): - as_bytes = input_ - else: - raise TypeError( - f"input must be a list[int], bytes, or ScaleBytes, not {type(input_)}" - ) - as_scale_bytes = ScaleBytes(as_bytes) - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) - return obj.decode() - - -def from_scale_encoding( - input_: Union[list[int], bytes, ScaleBytes], - type_name: ChainDataType, - is_vec: bool = False, - is_option: bool = False, -) -> Optional[dict]: - type_string = type_name.name - if type_name == ChainDataType.DelegatedInfo: - # DelegatedInfo is a tuple of (DelegateInfo, Compact) - type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" - if is_option: - type_string = f"Option<{type_string}>" - if is_vec: - type_string = f"Vec<{type_string}>" - - return from_scale_encoding_using_type_string(input_, type_string) - - -def decode_account_id(account_id_bytes: tuple): - # Convert the AccountId bytes to a Base64 string - return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) + DelegateInfo = 2 + NeuronInfoLite = 3 + StakeInfo = 4 + SubnetHyperparameters = 5 + DelegateInfoLite = 6 + DynamicInfo = 7 + ScheduledColdkeySwapInfo = 8 + SubnetInfo = 9 + SubnetState = 10 + SubnetIdentity = 11 def decode_hex_identity(info_dictionary): @@ -146,7 +96,32 @@ def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": @dataclass -class SubnetHyperparameters: +class InfoBase: + """Base dataclass for info objects.""" + + @abstractmethod + def _fix_decoded(self, decoded: Any) -> "InfoBase": + raise NotImplementedError( + "This is an abstract method and must be implemented in a subclass." + ) + + @classmethod + def from_any(cls, any_: Any) -> "InfoBase": + return cls._fix_decoded(any_) + + @classmethod + def list_from_any(cls, any_list: list[Any]) -> list["InfoBase"]: + return [cls.from_any(any_) for any_ in any_list] + + def __getitem__(self, item): + return getattr(self, item) + + def get(self, item, default=None): + return getattr(self, item, default) + + +@dataclass +class SubnetHyperparameters(InfoBase): """Dataclass for subnet hyperparameters.""" rho: int @@ -178,41 +153,44 @@ class SubnetHyperparameters: liquid_alpha_enabled: bool @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetHyperparameters"]: - decoded = bt_decode.SubnetHyperparameters.decode(vec_u8) + def _fix_decoded( + cls, decoded: Union[dict, "SubnetHyperparameters"] + ) -> "SubnetHyperparameters": return SubnetHyperparameters( - rho=decoded.rho, - kappa=decoded.kappa, - immunity_period=decoded.immunity_period, - min_allowed_weights=decoded.min_allowed_weights, - max_weight_limit=decoded.max_weights_limit, - tempo=decoded.tempo, - min_difficulty=decoded.min_difficulty, - max_difficulty=decoded.max_difficulty, - weights_version=decoded.weights_version, - weights_rate_limit=decoded.weights_rate_limit, - adjustment_interval=decoded.adjustment_interval, - activity_cutoff=decoded.activity_cutoff, - registration_allowed=decoded.registration_allowed, - target_regs_per_interval=decoded.target_regs_per_interval, - min_burn=decoded.min_burn, - max_burn=decoded.max_burn, - bonds_moving_avg=decoded.bonds_moving_avg, - max_regs_per_block=decoded.max_regs_per_block, - serving_rate_limit=decoded.serving_rate_limit, - max_validators=decoded.max_validators, - adjustment_alpha=decoded.adjustment_alpha, - difficulty=decoded.difficulty, - commit_reveal_weights_interval=decoded.commit_reveal_weights_interval, - commit_reveal_weights_enabled=decoded.commit_reveal_weights_enabled, - alpha_high=decoded.alpha_high, - alpha_low=decoded.alpha_low, - liquid_alpha_enabled=decoded.liquid_alpha_enabled, + rho=decoded.get("rho"), + kappa=decoded.get("kappa"), + immunity_period=decoded.get("immunity_period"), + min_allowed_weights=decoded.get("min_allowed_weights"), + max_weight_limit=decoded.get("max_weights_limit"), + tempo=decoded.get("tempo"), + min_difficulty=decoded.get("min_difficulty"), + max_difficulty=decoded.get("max_difficulty"), + weights_version=decoded.get("weights_version"), + weights_rate_limit=decoded.get("weights_rate_limit"), + adjustment_interval=decoded.get("adjustment_interval"), + activity_cutoff=decoded.get("activity_cutoff"), + registration_allowed=decoded.get("registration_allowed"), + target_regs_per_interval=decoded.get("target_regs_per_interval"), + min_burn=decoded.get("min_burn"), + max_burn=decoded.get("max_burn"), + bonds_moving_avg=decoded.get("bonds_moving_avg"), + max_regs_per_block=decoded.get("max_regs_per_block"), + serving_rate_limit=decoded.get("serving_rate_limit"), + max_validators=decoded.get("max_validators"), + adjustment_alpha=decoded.get("adjustment_alpha"), + difficulty=decoded.get("difficulty"), + commit_reveal_weights_interval=decoded.get( + "commit_reveal_weights_interval" + ), + commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), + alpha_high=decoded.get("alpha_high"), + alpha_low=decoded.get("alpha_low"), + liquid_alpha_enabled=decoded.get("liquid_alpha_enabled"), ) @dataclass -class StakeInfo: +class StakeInfo(InfoBase): """Dataclass for stake info.""" hotkey_ss58: str # Hotkey address @@ -225,64 +203,23 @@ class StakeInfo: is_registered: bool @classmethod - def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": - """Fixes the decoded values.""" - return cls( - hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), - coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), - netuid=int(decoded["netuid"]), - stake=Balance.from_rao(decoded["stake"]).set_unit(decoded["netuid"]), - locked=Balance.from_rao(decoded["locked"]).set_unit(decoded["netuid"]), - emission=Balance.from_rao(decoded["emission"]).set_unit(decoded["netuid"]), - drain=int(decoded["drain"]), - is_registered=bool(decoded["is_registered"]), - ) - - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["StakeInfo"]: - """Returns a StakeInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) - if decoded is None: - return None + def _fix_decoded(cls, decoded: Any) -> "StakeInfo": + hotkey = decode_account_id(decoded.get("hotkey")) + coldkey = decode_account_id(decoded.get("coldkey")) + netuid = int(decoded.get("netuid")) + stake = Balance.from_rao(decoded.get("stake")).set_unit(netuid) + locked = Balance.from_rao(decoded.get("locked")).set_unit(netuid) + emission = Balance.from_rao(decoded.get("emission")).set_unit(netuid) + drain = int(decoded.get("drain")) + is_registered = bool(decoded.get("is_registered")) - return StakeInfo.fix_decoded_values(decoded) - - @classmethod - def list_of_tuple_from_vec_u8( - cls, vec_u8: list[int] - ) -> dict[str, list["StakeInfo"]]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded: Optional[list[tuple[str, list[object]]]] = ( - from_scale_encoding_using_type_string( - vec_u8, type_string="Vec<(AccountId, Vec)>" - ) + return StakeInfo( + hotkey, coldkey, netuid, stake, locked, emission, drain, is_registered ) - if decoded is None: - return {} - - return { - ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ - StakeInfo.fix_decoded_values(d) for d in stake_info - ] - for account_id, stake_info in decoded - } - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) - if decoded is None: - return [] - - return [StakeInfo.fix_decoded_values(d) for d in decoded] - @dataclass -class PrometheusInfo: +class PrometheusInfo(InfoBase): """Dataclass for prometheus info.""" block: int @@ -292,15 +229,15 @@ class PrometheusInfo: ip_type: int @classmethod - def fix_decoded_values(cls, prometheus_info_decoded: dict) -> "PrometheusInfo": + def _fix_decoded(cls, decoded: Any) -> "PrometheusInfo": """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" - prometheus_info_decoded["ip"] = int_to_ip(int(prometheus_info_decoded["ip"])) + decoded["ip"] = int_to_ip(int(decoded["ip"])) - return cls(**prometheus_info_decoded) + return cls(**decoded) @dataclass -class NeuronInfo: +class NeuronInfo(InfoBase): """Dataclass for neuron metadata.""" hotkey: str @@ -371,49 +308,48 @@ def get_null_neuron() -> "NeuronInfo": return neuron @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": - n = bt_decode.NeuronInfo.decode(vec_u8) - stake_dict = process_stake_data(n.stake, n.netuid) + def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": + stake_dict = process_stake_data(decoded.stake) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) - axon_info = n.axon_info - coldkey = decode_account_id(n.coldkey) - hotkey = decode_account_id(n.hotkey) + axon_info = decoded.axon_info + coldkey = decode_account_id(decoded.get("coldkey")) + hotkey = decode_account_id(decoded.get("hotkey")) return NeuronInfo( hotkey=hotkey, coldkey=coldkey, - uid=n.uid, - netuid=n.netuid, - active=n.active, + uid=decoded.get("uid"), + netuid=decoded.get("netuid"), + active=decoded.get("active"), stake=total_stake, stake_dict=stake_dict, total_stake=total_stake, - rank=u16_normalized_float(n.rank), - emission=n.emission / 1e9, - incentive=u16_normalized_float(n.incentive), - consensus=u16_normalized_float(n.consensus), - trust=u16_normalized_float(n.trust), - validator_trust=u16_normalized_float(n.validator_trust), - dividends=u16_normalized_float(n.dividends), - last_update=n.last_update, - validator_permit=n.validator_permit, - weights=[[e[0], e[1]] for e in n.weights], - bonds=[[e[0], e[1]] for e in n.bonds], - pruning_score=n.pruning_score, + rank=u16_normalized_float(decoded.rget("ank")), + emission=decoded.get("emission") / 1e9, + incentive=u16_normalized_float(decoded.get("incentive")), + consensus=u16_normalized_float(decoded.get("consensus")), + trust=u16_normalized_float(decoded.get("trust")), + validator_trust=u16_normalized_float(decoded.get("validator_trust")), + dividends=u16_normalized_float(decoded.get("dividends")), + last_update=decoded.get("last_update"), + validator_permit=decoded.get("validator_permit"), + weights=[[e[0], e[1]] for e in decoded.get("weights")], + bonds=[[e[0], e[1]] for e in decoded.get("bonds")], + pruning_score=decoded.get("pruning_score"), prometheus_info=PrometheusInfo( - block=n.prometheus_info.block, - version=n.prometheus_info.version, - ip=str(netaddr.IPAddress(n.prometheus_info.ip)), - port=n.prometheus_info.port, - ip_type=n.prometheus_info.ip_type, + block=decoded.get("prometheus_info").get("block"), + version=decoded.get("prometheus_info").get("version"), + ip=str(netaddr.IPAddress(decoded.get("prometheus_info").get("ip"))), + port=decoded.get("prometheus_info").get("port"), + ip_type=decoded.get("prometheus_info").get("ip_type"), ), axon_info=AxonInfo( - version=axon_info.version, - ip=str(netaddr.IPAddress(axon_info.ip)), - port=axon_info.port, - ip_type=axon_info.ip_type, - placeholder1=axon_info.placeholder1, - placeholder2=axon_info.placeholder2, - protocol=axon_info.protocol, + version=axon_info.get("version"), + ip=str(netaddr.IPAddress(axon_info.get("ip"))), + port=axon_info.get("port"), + ip_type=axon_info.get("ip_type"), + placeholder1=axon_info.get("placeholder1"), + placeholder2=axon_info.get("placeholder2"), + protocol=axon_info.get("protocol"), hotkey=hotkey, coldkey=coldkey, ), @@ -422,7 +358,7 @@ def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": @dataclass -class NeuronInfoLite: +class NeuronInfoLite(InfoBase): """Dataclass for neuron metadata, but without the weights and bonds.""" hotkey: str @@ -476,78 +412,71 @@ def get_null_neuron() -> "NeuronInfoLite": return neuron @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfoLite"]: - decoded = bt_decode.NeuronInfoLite.decode_vec(vec_u8) - results = [] - for item in decoded: - active = item.active - axon_info = item.axon_info - coldkey = decode_account_id(item.coldkey) - consensus = item.consensus - dividends = item.dividends - emission = item.emission - hotkey = decode_account_id(item.hotkey) - incentive = item.incentive - last_update = item.last_update - netuid = item.netuid - prometheus_info = item.prometheus_info - pruning_score = item.pruning_score - rank = item.rank - stake_dict = process_stake_data(item.stake, item.netuid) - stake = ( - sum(stake_dict.values()) - if stake_dict - else Balance(0).set_unit(item.netuid) - ) - trust = item.trust - uid = item.uid - validator_permit = item.validator_permit - validator_trust = item.validator_trust - results.append( - NeuronInfoLite( - active=active, - axon_info=AxonInfo( - version=axon_info.version, - ip=str(netaddr.IPAddress(axon_info.ip)), - port=axon_info.port, - ip_type=axon_info.ip_type, - placeholder1=axon_info.placeholder1, - placeholder2=axon_info.placeholder2, - protocol=axon_info.protocol, - hotkey=hotkey, - coldkey=coldkey, - ), - coldkey=coldkey, - consensus=u16_normalized_float(consensus), - dividends=u16_normalized_float(dividends), - emission=emission / 1e9, - hotkey=hotkey, - incentive=u16_normalized_float(incentive), - last_update=last_update, - netuid=netuid, - prometheus_info=PrometheusInfo( - version=prometheus_info.version, - ip=str(netaddr.IPAddress(prometheus_info.ip)), - port=prometheus_info.port, - ip_type=prometheus_info.ip_type, - block=prometheus_info.block, - ), - pruning_score=pruning_score, - rank=u16_normalized_float(rank), - stake_dict=stake_dict, - stake=stake, - total_stake=stake, - trust=u16_normalized_float(trust), - uid=uid, - validator_permit=validator_permit, - validator_trust=u16_normalized_float(validator_trust), - ) - ) - return results + def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite": + active = decoded.get("active") + axon_info = decoded.get("axon_info") + coldkey = decode_account_id(decoded.get("coldkey")) + consensus = decoded.get("consensus") + dividends = decoded.get("dividends") + emission = decoded.get("emission") + hotkey = decode_account_id(decoded.get("hotkey")) + incentive = decoded.get("incentive") + last_update = decoded.get("last_update") + netuid = decoded.get("netuid") + prometheus_info = decoded.get("prometheus_info") + pruning_score = decoded.get("pruning_score") + rank = decoded.get("rank") + stake_dict = process_stake_data(decoded.get("stake")) + stake = sum(stake_dict.values()) if stake_dict else Balance(0) + trust = decoded.get("trust") + uid = decoded.get("uid") + validator_permit = decoded.get("validator_permit") + validator_trust = decoded.get("validator_trust") + + neuron = cls( + active=active, + axon_info=AxonInfo( + version=axon_info.version, + ip=str(netaddr.IPAddress(axon_info.ip)), + port=axon_info.port, + ip_type=axon_info.ip_type, + placeholder1=axon_info.placeholder1, + placeholder2=axon_info.placeholder2, + protocol=axon_info.protocol, + hotkey=hotkey, + coldkey=coldkey, + ), + coldkey=coldkey, + consensus=u16_normalized_float(consensus), + dividends=u16_normalized_float(dividends), + emission=emission / 1e9, + hotkey=hotkey, + incentive=u16_normalized_float(incentive), + last_update=last_update, + netuid=netuid, + prometheus_info=PrometheusInfo( + version=prometheus_info.version, + ip=str(netaddr.IPAddress(prometheus_info.ip)), + port=prometheus_info.port, + ip_type=prometheus_info.ip_type, + block=prometheus_info.block, + ), + pruning_score=pruning_score, + rank=u16_normalized_float(rank), + stake_dict=stake_dict, + stake=stake, + total_stake=stake, + trust=u16_normalized_float(trust), + uid=uid, + validator_permit=validator_permit, + validator_trust=u16_normalized_float(validator_trust), + ) + + return neuron @dataclass -class DelegateInfo: +class DelegateInfo(InfoBase): """ Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. @@ -578,15 +507,14 @@ class DelegateInfo: total_daily_return: Balance # Total daily return of the delegate @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> Optional["DelegateInfo"]: - decoded = bt_decode.DelegateInfo.decode(vec_u8) - hotkey = decode_account_id(decoded.delegate_ss58) + def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": + hotkey = decode_account_id(decoded.hotkey_ss58) owner = decode_account_id(decoded.owner_ss58) nominators = [ (decode_account_id(x), Balance.from_rao(y)) for x, y in decoded.nominators ] total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - return DelegateInfo( + return cls( hotkey_ss58=hotkey, total_stake=total_stake, nominators=nominators, @@ -598,60 +526,9 @@ def from_vec_u8(cls, vec_u8: bytes) -> Optional["DelegateInfo"]: total_daily_return=Balance.from_rao(decoded.total_daily_return), ) - @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["DelegateInfo"]: - decoded = bt_decode.DelegateInfo.decode_vec(vec_u8) - results = [] - for d in decoded: - hotkey = decode_account_id(d.delegate_ss58) - owner = decode_account_id(d.owner_ss58) - nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in d.nominators - ] - total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - results.append( - DelegateInfo( - hotkey_ss58=hotkey, - total_stake=total_stake, - nominators=nominators, - owner_ss58=owner, - take=u16_normalized_float(d.take), - validator_permits=d.validator_permits, - registrations=d.registrations, - return_per_1000=Balance.from_rao(d.return_per_1000), - total_daily_return=Balance.from_rao(d.total_daily_return), - ) - ) - return results - - @classmethod - def delegated_list_from_vec_u8( - cls, vec_u8: bytes - ) -> list[tuple["DelegateInfo", Balance]]: - decoded = bt_decode.DelegateInfo.decode_delegated(vec_u8) - results = [] - for d, b in decoded: - nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in d.nominators - ] - total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - delegate = DelegateInfo( - hotkey_ss58=decode_account_id(d.delegate_ss58), - total_stake=total_stake, - nominators=nominators, - owner_ss58=decode_account_id(d.owner_ss58), - take=u16_normalized_float(d.take), - validator_permits=d.validator_permits, - registrations=d.registrations, - return_per_1000=Balance.from_rao(d.return_per_1000), - total_daily_return=Balance.from_rao(d.total_daily_return), - ) - results.append((delegate, Balance.from_rao(b))) - return results - @dataclass -class DelegateInfoLite: +class DelegateInfoLite(InfoBase): """ Dataclass for light delegate information. @@ -671,9 +548,9 @@ class DelegateInfoLite: owner_stake: Balance # Own stake of the delegate @classmethod - def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": + def _fix_decoded(cls, decoded: Any) -> "DelegateInfoLite": """Fixes the decoded values.""" - decoded_take = decoded["take"] + decoded_take = decoded.get("take") if decoded_take == 65535: fixed_take = None @@ -681,46 +558,17 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": fixed_take = u16_normalized_float(decoded_take) return cls( - hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), - owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + hotkey_ss58=ss58_encode(decoded.get("delegate_ss58"), SS58_FORMAT), + owner_ss58=ss58_encode(decoded.get("owner_ss58"), SS58_FORMAT), take=fixed_take, - total_stake=Balance.from_rao(decoded["total_stake"]), - owner_stake=Balance.from_rao(decoded["owner_stake"]), + total_stake=Balance.from_rao(decoded.get("total_stake")), + owner_stake=Balance.from_rao(decoded.get("owner_stake")), previous_total_stake=None, ) - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfoLite"]: - """Returns a DelegateInfoLite object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLite) - - if decoded is None: - return None - - decoded = DelegateInfoLite.fix_decoded_values(decoded) - - return decoded - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfoLite"]: - """Returns a list of DelegateInfoLite objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.DelegateInfoLite, is_vec=True - ) - - if decoded is None: - return [] - - decoded = [DelegateInfoLite.fix_decoded_values(d) for d in decoded] - - return decoded - @dataclass -class SubnetInfo: +class SubnetInfo(InfoBase): """Dataclass for subnet info.""" netuid: int @@ -743,117 +591,35 @@ class SubnetInfo: owner_ss58: str @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: - decoded = bt_decode.SubnetInfo.decode_vec_option(vec_u8) - result = [] - for d in decoded: - result.append( - SubnetInfo( - netuid=d.netuid, - rho=d.rho, - kappa=d.kappa, - difficulty=d.difficulty, - immunity_period=d.immunity_period, - max_allowed_validators=d.max_allowed_validators, - min_allowed_weights=d.min_allowed_weights, - max_weight_limit=d.max_weights_limit, - scaling_law_power=d.scaling_law_power, - subnetwork_n=d.subnetwork_n, - max_n=d.max_allowed_uids, - blocks_since_epoch=d.blocks_since_last_step, - tempo=d.tempo, - modality=d.network_modality, - connection_requirements={ - str(int(netuid)): u16_normalized_float(int(req)) - for (netuid, req) in d.network_connect - }, - emission_value=d.emission_values, - burn=Balance.from_rao(d.burn), - owner_ss58=decode_account_id(d.owner), - ) - ) - return result - - -@dataclass -class SubnetInfoV2: - """Dataclass for subnet info.""" - - netuid: int - owner_ss58: str - max_allowed_validators: int - scaling_law_power: float - subnetwork_n: int - max_n: int - blocks_since_epoch: int - modality: int - emission_value: float - burn: Balance - tao_locked: Balance - hyperparameters: "SubnetHyperparameters" - dynamic_pool: "DynamicPool" - - @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetInfoV2"]: - """Returns a SubnetInfoV2 object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - decoded = bt_decode.SubnetInfoV2.decode(vec_u8) # TODO fix values - - if decoded is None: - return None - - return cls.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfoV2"]: - """Returns a list of SubnetInfoV2 objects from a ``vec_u8``.""" - decoded = bt_decode.SubnetInfoV2.decode_vec(vec_u8) # TODO fix values - - if decoded is None: - return [] - - decoded = [cls.fix_decoded_values(d) for d in decoded] - - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "SubnetInfoV2": - """Returns a SubnetInfoV2 object from a decoded SubnetInfoV2 dictionary.""" - # init dynamic pool object - pool_info = decoded["dynamic_pool"] - if pool_info: - pool = DynamicPool( - True, - pool_info["netuid"], - pool_info["alpha_issuance"], - pool_info["alpha_outstanding"], - pool_info["alpha_reserve"], - pool_info["tao_reserve"], - pool_info["k"], - ) - else: - pool = DynamicPool(False, decoded["netuid"], 0, 0, 0, 0, 0) - - return SubnetInfoV2( - netuid=decoded["netuid"], - owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), - max_allowed_validators=decoded["max_allowed_validators"], - scaling_law_power=decoded["scaling_law_power"], - subnetwork_n=decoded["subnetwork_n"], - max_n=decoded["max_allowed_uids"], - blocks_since_epoch=decoded["blocks_since_last_step"], - modality=decoded["network_modality"], - emission_value=decoded["emission_values"], - burn=Balance.from_rao(decoded["burn"]), - tao_locked=Balance.from_rao(decoded["tao_locked"]), - hyperparameters=decoded["hyperparameters"], - dynamic_pool=pool, + def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": + d = decoded + return SubnetInfo( + netuid=d.netuid, + rho=d.rho, + kappa=d.kappa, + difficulty=d.difficulty, + immunity_period=d.immunity_period, + max_allowed_validators=d.max_allowed_validators, + min_allowed_weights=d.min_allowed_weights, + max_weight_limit=d.max_weights_limit, + scaling_law_power=d.scaling_law_power, + subnetwork_n=d.subnetwork_n, + max_n=d.max_allowed_uids, + blocks_since_epoch=d.blocks_since_last_step, + tempo=d.tempo, + modality=d.network_modality, + connection_requirements={ + str(int(netuid)): u16_normalized_float(int(req)) + for (netuid, req) in d.network_connect + }, + emission_value=d.emission_values, + burn=Balance.from_rao(d.burn), + owner_ss58=decode_account_id(d.owner), ) @dataclass -class SubnetIdentity: +class SubnetIdentity(InfoBase): """Dataclass for subnet identity information.""" subnet_name: str @@ -861,14 +627,7 @@ class SubnetIdentity: subnet_contact: str @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetIdentity"]: - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetIdentity) - if decoded is None: - return None - + def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": return SubnetIdentity( subnet_name=bytes(decoded["subnet_name"]).decode(), github_repo=bytes(decoded["github_repo"]).decode(), @@ -877,7 +636,7 @@ def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetIdentity"]: @dataclass -class DynamicInfo: +class DynamicInfo(InfoBase): netuid: int owner_hotkey: str owner_coldkey: str @@ -902,54 +661,33 @@ class DynamicInfo: subnet_identity: Optional[SubnetIdentity] @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: - if len(vec_u8) == 0: - return None - decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo) - if decoded is None: - return None - return DynamicInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo"]: - decoded = from_scale_encoding( - vec_u8, ChainDataType.DynamicInfo, is_vec=True, is_option=True - ) - if decoded is None: - return [] - decoded = [DynamicInfo.fix_decoded_values(d) for d in decoded] - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": + def _fix_decoded(cls, decoded: Any) -> "DynamicInfo": """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" - netuid = int(decoded["netuid"]) - symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() - subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() - is_dynamic = ( - True if int(decoded["netuid"]) > 0 else False - ) # TODO: Patching this temporarily for netuid 0 + netuid = int(decoded.get("netuid")) + symbol = bytes([int(b) for b in decoded.get("token_symbol")]).decode() + subnet_name = bytes([int(b) for b in decoded.get("subnet_name")]).decode() + is_dynamic = True if netuid > 0 else False # Patching for netuid 0 - owner_hotkey = ss58_encode(decoded["owner_hotkey"], SS58_FORMAT) - owner_coldkey = ss58_encode(decoded["owner_coldkey"], SS58_FORMAT) + owner_hotkey = decode_account_id(decoded.get("owner_hotkey")) + owner_coldkey = decode_account_id(decoded.get("owner_coldkey")) - emission = Balance.from_rao(decoded["emission"]).set_unit(0) - alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) - alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) - tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) - alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( - netuid - ) - alpha_in_emission = Balance.from_rao(decoded["alpha_in_emission"]).set_unit( + emission = Balance.from_rao(decoded.get("emission")).set_unit(0) + alpha_in = Balance.from_rao(decoded.get("alpha_in")).set_unit(netuid) + alpha_out = Balance.from_rao(decoded.get("alpha_out")).set_unit(netuid) + tao_in = Balance.from_rao(decoded.get("tao_in")).set_unit(0) + alpha_out_emission = Balance.from_rao( + decoded.get("alpha_out_emission") + ).set_unit(netuid) + alpha_in_emission = Balance.from_rao(decoded.get("alpha_in_emission")).set_unit( netuid ) - tao_in_emission = Balance.from_rao(decoded["tao_in_emission"]).set_unit(0) + tao_in_emission = Balance.from_rao(decoded.get("tao_in_emission")).set_unit(0) pending_alpha_emission = Balance.from_rao( - decoded["pending_alpha_emission"] + decoded.get("pending_alpha_emission") ).set_unit(netuid) pending_root_emission = Balance.from_rao( - decoded["pending_root_emission"] + decoded.get("pending_root_emission") ).set_unit(0) price = ( Balance.from_tao(1.0) @@ -960,11 +698,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": ) # TODO: Patching this temporarily for netuid 0 if decoded.get("subnet_identity"): - subnet_identity = SubnetIdentity( - subnet_name=decoded["subnet_identity"]["subnet_name"], - github_repo=decoded["subnet_identity"]["github_repo"], - subnet_contact=decoded["subnet_identity"]["subnet_contact"], - ) + subnet_identity = SubnetIdentity.from_any(decoded.get("subnet_identity")) else: subnet_identity = None @@ -974,9 +708,9 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": owner_coldkey=owner_coldkey, subnet_name=subnet_name, symbol=symbol, - tempo=int(decoded["tempo"]), - last_step=int(decoded["last_step"]), - blocks_since_last_step=int(decoded["blocks_since_last_step"]), + tempo=int(decoded.get("tempo")), + last_step=int(decoded.get("last_step")), + blocks_since_last_step=int(decoded.get("blocks_since_last_step")), emission=emission, alpha_in=alpha_in, alpha_out=alpha_out, @@ -989,7 +723,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": tao_in_emission=tao_in_emission, pending_alpha_emission=pending_alpha_emission, pending_root_emission=pending_root_emission, - network_registered_at=int(decoded["network_registered_at"]), + network_registered_at=int(decoded.get("network_registered_at")), subnet_identity=subnet_identity, ) @@ -1078,166 +812,7 @@ def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: @dataclass -class DynamicPoolInfoV2: - """Dataclass for dynamic pool info.""" - - netuid: int - alpha_issuance: int - alpha_outstanding: int - alpha_reserve: int - tao_reserve: int - k: int - - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicPoolInfoV2"]: - """Returns a DynamicPoolInfoV2 object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - return from_scale_encoding(vec_u8, ChainDataType.DynamicPoolInfoV2) - - -@dataclass -class DynamicPool: - is_dynamic: bool - alpha_issuance: Balance - alpha_outstanding: Balance - alpha_reserve: Balance - tao_reserve: Balance - k: int - price: Balance - netuid: int - - def __init__( - self, - is_dynamic: bool, - netuid: int, - alpha_issuance: Union[int, Balance], - alpha_outstanding: Union[int, Balance], - alpha_reserve: Union[int, Balance], - tao_reserve: Union[int, Balance], - k: int, - ): - self.is_dynamic = is_dynamic - self.netuid = netuid - self.alpha_issuance = ( - alpha_issuance - if isinstance(alpha_issuance, Balance) - else Balance.from_rao(alpha_issuance).set_unit(netuid) - ) - self.alpha_outstanding = ( - alpha_outstanding - if isinstance(alpha_outstanding, Balance) - else Balance.from_rao(alpha_outstanding).set_unit(netuid) - ) - self.alpha_reserve = ( - alpha_reserve - if isinstance(alpha_reserve, Balance) - else Balance.from_rao(alpha_reserve).set_unit(netuid) - ) - self.tao_reserve = ( - tao_reserve - if isinstance(tao_reserve, Balance) - else Balance.from_rao(tao_reserve).set_unit(0) - ) - self.k = k - if is_dynamic: - if self.alpha_reserve.tao > 0: - self.price = Balance.from_tao( - self.tao_reserve.tao / self.alpha_reserve.tao - ) - else: - self.price = Balance.from_tao(0.0) - else: - self.price = Balance.from_tao(1.0) - - def __str__(self) -> str: - return ( - f"DynamicPool( alpha_issuance={self.alpha_issuance}, " - f"alpha_outstanding={self.alpha_outstanding}, " - f"alpha_reserve={self.alpha_reserve}, " - f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )" - ) - - def __repr__(self) -> str: - return self.__str__() - - def tao_to_alpha(self, tao: Balance) -> Balance: - if self.price.tao != 0: - return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) - else: - return Balance.from_tao(0) - - def alpha_to_tao(self, alpha: Balance) -> Balance: - return Balance.from_tao(alpha.tao * self.price.tao) - - def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: - """ - Returns an estimate of how much Alpha would a staker receive if they stake their tao - using the current pool state - Args: - tao: Amount of TAO to stake. - Returns: - Tuple of balances where the first part is the amount of Alpha received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_tao_in = self.tao_reserve + tao - if new_tao_in == 0: - return tao, Balance.from_rao(0) - new_alpha_in = self.k / new_tao_in - - # Amount of alpha given to the staker - alpha_returned = Balance.from_rao( - self.alpha_reserve.rao - new_alpha_in.rao - ).set_unit(self.netuid) - - # Ideal conversion as if there is no slippage, just price - alpha_ideal = self.tao_to_alpha(tao) - - if alpha_ideal.tao > alpha_returned.tao: - slippage = Balance.from_tao( - alpha_ideal.tao - alpha_returned.tao - ).set_unit(self.netuid) - else: - slippage = Balance.from_tao(0) - else: - alpha_returned = tao.set_unit(self.netuid) - slippage = Balance.from_tao(0) - return alpha_returned, slippage - - def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: - """ - Returns an estimate of how much TAO would a staker receive if they unstake their - alpha using the current pool state - Args: - alpha: Amount of Alpha to stake. - Returns: - Tuple of balances where the first part is the amount of TAO received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_alpha_in = self.alpha_reserve + alpha - new_tao_reserve = self.k / new_alpha_in - # Amount of TAO given to the unstaker - tao_returned = Balance.from_rao(self.tao_reserve - new_tao_reserve) - - # Ideal conversion as if there is no slippage, just price - tao_ideal = self.alpha_to_tao(alpha) - - if tao_ideal > tao_returned: - slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) - else: - slippage = Balance.from_tao(0) - else: - tao_returned = alpha.set_unit(0) - slippage = Balance.from_tao(0) - return tao_returned, slippage - - -@dataclass -class ScheduledColdkeySwapInfo: +class ScheduledColdkeySwapInfo(InfoBase): """Dataclass for scheduled coldkey swap information.""" old_coldkey: str @@ -1245,50 +820,17 @@ class ScheduledColdkeySwapInfo: arbitration_block: int @classmethod - def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + def _fix_decoded(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": """Fixes the decoded values.""" return cls( - old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), - new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), - arbitration_block=decoded["arbitration_block"], - ) - - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["ScheduledColdkeySwapInfo"]: - """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) - if decoded is None: - return None - - return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["ScheduledColdkeySwapInfo"]: - """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + old_coldkey=decode_account_id(decoded.get("old_coldkey")), + new_coldkey=decode_account_id(decoded.get("new_coldkey")), + arbitration_block=decoded.get("arbitration_block"), ) - if decoded is None: - return [] - - return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: - """Decodes a list of AccountIds from vec_u8.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True - ) - if decoded is None: - return None - return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] @dataclass -class SubnetState: +class SubnetState(InfoBase): netuid: int hotkeys: list[str] coldkeys: list[str] @@ -1309,349 +851,38 @@ class SubnetState: emission_history: list[list[int]] @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: - if len(vec_u8) == 0: - return None - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState) - if decoded is None: - return None - return SubnetState.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True - ) - if decoded is None: - return [] - decoded = [SubnetState.fix_decoded_values(d) for d in decoded] - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "SubnetState": - netuid = decoded["netuid"] + def _fix_decoded(cls, decoded: Any) -> "SubnetState": + netuid = decoded.get("netuid") return SubnetState( netuid=netuid, - hotkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], - coldkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], - active=decoded["active"], - validator_permit=decoded["validator_permit"], + hotkeys=[decode_account_id(val) for val in decoded.get("hotkeys")], + coldkeys=[decode_account_id(val) for val in decoded.get("coldkeys")], + active=decoded.get("active"), + validator_permit=decoded.get("validator_permit"), pruning_score=[ - u16_normalized_float(val) for val in decoded["pruning_score"] + u16_normalized_float(val) for val in decoded.get("pruning_score") ], - last_update=decoded["last_update"], + last_update=decoded.get("last_update"), emission=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["emission"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("emission") ], - dividends=[u16_normalized_float(val) for val in decoded["dividends"]], - incentives=[u16_normalized_float(val) for val in decoded["incentives"]], - consensus=[u16_normalized_float(val) for val in decoded["consensus"]], - trust=[u16_normalized_float(val) for val in decoded["trust"]], - rank=[u16_normalized_float(val) for val in decoded["rank"]], - block_at_registration=decoded["block_at_registration"], + dividends=[u16_normalized_float(val) for val in decoded.get("dividends")], + incentives=[u16_normalized_float(val) for val in decoded.get("incentives")], + consensus=[u16_normalized_float(val) for val in decoded.get("consensus")], + trust=[u16_normalized_float(val) for val in decoded.get("trust")], + rank=[u16_normalized_float(val) for val in decoded.get("rank")], + block_at_registration=decoded.get("block_at_registration"), alpha_stake=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["alpha_stake"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("alpha_stake") ], tao_stake=[ - Balance.from_rao(val).set_unit(0) for val in decoded["tao_stake"] + Balance.from_rao(val).set_unit(0) for val in decoded.get("tao_stake") ], total_stake=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["total_stake"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("total_stake") ], - emission_history=decoded["emission_history"], + emission_history=decoded.get("emission_history"), ) - - -class SubstakeElements: - @staticmethod - def decode(result: list[int]) -> list[dict]: - descaled = from_scale_encoding( - input_=result, type_name=ChainDataType.SubstakeElements, is_vec=True - ) - result = [] - for item in descaled: - result.append( - { - "hotkey": ss58_encode(item["hotkey"], SS58_FORMAT), - "coldkey": ss58_encode(item["coldkey"], SS58_FORMAT), - "netuid": item["netuid"], - "stake": Balance.from_rao(item["stake"]), - } - ) - return result - - -custom_rpc_type_registry = { - "types": { - "SubnetInfo": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["rho", "Compact"], - ["kappa", "Compact"], - ["difficulty", "Compact"], - ["immunity_period", "Compact"], - ["max_allowed_validators", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["scaling_law_power", "Compact"], - ["subnetwork_n", "Compact"], - ["max_allowed_uids", "Compact"], - ["blocks_since_last_step", "Compact"], - ["tempo", "Compact"], - ["network_modality", "Compact"], - ["network_connect", "Vec<[u16; 2]>"], - ["emission_values", "Compact"], - ["burn", "Compact"], - ["owner", "AccountId"], - ], - }, - "DynamicPoolInfoV2": { - "type": "struct", - "type_mapping": [ - ["netuid", "u16"], - ["alpha_issuance", "u64"], - ["alpha_outstanding", "u64"], - ["alpha_reserve", "u64"], - ["tao_reserve", "u64"], - ["k", "u128"], - ], - }, - "SubnetInfoV2": { - "type": "struct", - "type_mapping": [ - ["netuid", "u16"], - ["owner", "AccountId"], - ["max_allowed_validators", "u16"], - ["scaling_law_power", "u16"], - ["subnetwork_n", "u16"], - ["max_allowed_uids", "u16"], - ["blocks_since_last_step", "Compact"], - ["network_modality", "u16"], - ["emission_values", "Compact"], - ["burn", "Compact"], - ["tao_locked", "Compact"], - ["hyperparameters", "SubnetHyperparameters"], - ["dynamic_pool", "Option"], - ], - }, - "DelegateInfo": { - "type": "struct", - "type_mapping": [ - ["delegate_ss58", "AccountId"], - ["take", "Compact"], - ["nominators", "Vec<(AccountId, Compact)>"], - ["owner_ss58", "AccountId"], - ["registrations", "Vec>"], - ["validator_permits", "Vec>"], - ["return_per_1000", "Compact"], - ["total_daily_return", "Compact"], - ], - }, - "DelegateInfoLite": { - "type": "struct", - "type_mapping": [ - ["delegate_ss58", "AccountId"], - ["owner_ss58", "AccountId"], - ["take", "u16"], - ["owner_stake", "Compact"], - ["total_stake", "Compact"], - ], - }, - "NeuronInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["weights", "Vec<(Compact, Compact)>"], - ["bonds", "Vec<(Compact, Compact)>"], - ["pruning_score", "Compact"], - ], - }, - "NeuronInfoLite": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["pruning_score", "Compact"], - ], - }, - "axon_info": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ["protocol", "u8"], - ["placeholder1", "u8"], - ["placeholder2", "u8"], - ], - }, - "PrometheusInfo": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ], - }, - "IPInfo": { - "type": "struct", - "type_mapping": [ - ["ip", "Compact"], - ["ip_type_and_protocol", "Compact"], - ], - }, - "ScheduledColdkeySwapInfo": { - "type": "struct", - "type_mapping": [ - ["old_coldkey", "AccountId"], - ["new_coldkey", "AccountId"], - ["arbitration_block", "Compact"], - ], - }, - "SubnetState": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["hotkeys", "Vec"], - ["coldkeys", "Vec"], - ["active", "Vec"], - ["validator_permit", "Vec"], - ["pruning_score", "Vec>"], - ["last_update", "Vec>"], - ["emission", "Vec>"], - ["dividends", "Vec>"], - ["incentives", "Vec>"], - ["consensus", "Vec>"], - ["trust", "Vec>"], - ["rank", "Vec>"], - ["block_at_registration", "Vec>"], - ["alpha_stake", "Vec>"], - ["tao_stake", "Vec>"], - ["total_stake", "Vec>"], - ["emission_history", "Vec>>"], - ], - }, - "StakeInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["netuid", "Compact"], - ["stake", "Compact"], - ["locked", "Compact"], - ["emission", "Compact"], - ["drain", "Compact"], - ["is_registered", "bool"], - ], - }, - "DynamicInfo": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["owner_hotkey", "AccountId"], - ["owner_coldkey", "AccountId"], - ["subnet_name", "Vec>"], - ["token_symbol", "Vec>"], - ["tempo", "Compact"], - ["last_step", "Compact"], - ["blocks_since_last_step", "Compact"], - ["emission", "Compact"], - ["alpha_in", "Compact"], - ["alpha_out", "Compact"], - ["tao_in", "Compact"], - ["alpha_out_emission", "Compact"], - ["alpha_in_emission", "Compact"], - ["tao_in_emission", "Compact"], - ["pending_alpha_emission", "Compact"], - ["pending_root_emission", "Compact"], - ["network_registered_at", "Compact"], - ["subnet_identity", "Option"], - ], - }, - "SubstakeElements": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["netuid", "Compact"], - ["stake", "Compact"], - ], - }, - "SubnetHyperparameters": { - "type": "struct", - "type_mapping": [ - ["rho", "Compact"], - ["kappa", "Compact"], - ["immunity_period", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["tempo", "Compact"], - ["min_difficulty", "Compact"], - ["max_difficulty", "Compact"], - ["weights_version", "Compact"], - ["weights_rate_limit", "Compact"], - ["adjustment_interval", "Compact"], - ["activity_cutoff", "Compact"], - ["registration_allowed", "bool"], - ["target_regs_per_interval", "Compact"], - ["min_burn", "Compact"], - ["max_burn", "Compact"], - ["bonds_moving_avg", "Compact"], - ["max_regs_per_block", "Compact"], - ["serving_rate_limit", "Compact"], - ["max_validators", "Compact"], - ["adjustment_alpha", "Compact"], - ["difficulty", "Compact"], - ["commit_reveal_weights_interval", "Compact"], - ["commit_reveal_weights_enabled", "bool"], - ["alpha_high", "Compact"], - ["alpha_low", "Compact"], - ["liquid_alpha_enabled", "bool"], - ], - }, - "SubnetIdentity": { - "type": "struct", - "type_mapping": [ - ["subnet_name", "Vec"], - ["github_repo", "Vec"], - ["subnet_contact", "Vec"], - ], - }, - } -} diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index ae5db605..721ce8f2 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -495,21 +495,12 @@ async def get_neuron_for_pubkey_and_subnet(): if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neuron", - params=[netuid, uid], + result = await subtensor.neuron_for_uid( + uid=uid, + netuid=netuid, + block_hash=subtensor.substrate.last_block_hash, ) - - if not (result := hex_bytes_result): - return NeuronInfo.get_null_neuron() - - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return NeuronInfo.from_vec_u8(bytes_result) + return result print_verbose("Checking subnet status") if not await subtensor.subnet_exists(netuid): @@ -776,7 +767,7 @@ async def burned_register_extrinsic( console.print( "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance[wallet.coldkey.ss58_address]}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) if len(netuids_for_hotkey) > 0: @@ -925,8 +916,8 @@ async def run_faucet_extrinsic( wallet.coldkeypub.ss58_address ) console.print( - f"Balance: [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right:" - f" [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" + f"Balance: [blue]{old_balance}[/blue] :arrow_right:" + f" [green]{new_balance}[/green]" ) old_balance = new_balance diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 9d0d5a72..95e29181 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -61,14 +61,14 @@ async def get_transfer_fee() -> Balance: call=call, keypair=wallet.coldkeypub ) except SubstrateRequestException as e: - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao err_console.print( - f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n" - f" {format_error_message(e, subtensor.substrate)}[/bold white]\n" - f" Defaulting to default transfer fee: {payment_info['partialFee']}" + f":cross_mark: [red]Failed to get payment info[/red]:\n" + f" [bold white]{format_error_message(e)}[/bold white]\n" + f" Defaulting to default transfer fee: {payment_info['partial_fee']}" ) - return Balance.from_rao(payment_info["partialFee"]) + return Balance.from_rao(payment_info["partial_fee"]) async def do_transfer() -> tuple[bool, str, str]: """ @@ -122,13 +122,12 @@ async def do_transfer() -> tuple[bool, str, str]: # check existential deposit and fee print_verbose("Fetching existential and fee", status) block_hash = await subtensor.substrate.get_chain_head() - account_balance_, existential_deposit = await asyncio.gather( + account_balance, existential_deposit = await asyncio.gather( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.get_existential_deposit(block_hash=block_hash), ) - account_balance = account_balance_[wallet.coldkeypub.ss58_address] fee = await get_transfer_fee() if not keep_alive: @@ -184,7 +183,7 @@ async def do_transfer() -> tuple[bool, str, str]: ) console.print( f"Balance:\n" - f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" + f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) return True diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a5f79776..63eae82e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -7,8 +7,6 @@ from bittensor_wallet.utils import SS58_FORMAT import scalecodec from scalecodec import GenericCall -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset from substrateinterface.exceptions import SubstrateRequestException import typer @@ -18,7 +16,6 @@ ) from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, - custom_rpc_type_registry, StakeInfo, NeuronInfoLite, NeuronInfo, @@ -33,7 +30,6 @@ from bittensor_cli.src.bittensor.balances import Balance, FixedPoint, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY from bittensor_cli.src.bittensor.utils import ( - ss58_to_vec_u8, format_error_message, console, err_console, @@ -41,6 +37,8 @@ validate_chain_endpoint, u16_normalized_float, u64_normalized_float, + ss58_to_vec_u8, + encode_account_id ) @@ -212,21 +210,13 @@ async def get_delegates( :return: List of DelegateInfo objects, or an empty list if there are no delegates. """ - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="DelegateInfoRuntimeApi", method="get_delegates", params=[], block_hash=block_hash, ) - if hex_bytes_result is not None: - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return DelegateInfo.list_from_vec_u8(bytes_result) - else: - return [] + return DelegateInfo.list_from_any(result) if result is not None else [] async def get_stake_for_coldkey( self, @@ -247,25 +237,18 @@ async def get_stake_for_coldkey( Stake information is vital for account holders to assess their investment and participation in the network's delegation and consensus processes. """ - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkey", - params=[encoded_coldkey], + params=[coldkey_ss58], block_hash=block_hash, reuse_block=reuse_block, ) - if hex_bytes_result is None: + if result is None: return [] - - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - stakes = StakeInfo.list_from_vec_u8(bytes_result) + stakes = StakeInfo.list_from_any(result) return [stake for stake in stakes if stake.stake > 0] async def get_stake_for_coldkey_and_hotkey( @@ -325,10 +308,10 @@ async def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[list[int]], dict[str, int]]], + params: Optional[Union[list[list[int]], list[int], dict[str, int]]], block_hash: Optional[str] = None, reuse_block: Optional[bool] = False, - ) -> Optional[str]: + ) -> Optional[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. This function is essential for advanced users @@ -340,45 +323,42 @@ async def query_runtime_api( :param block_hash: The hash of the blockchain block number at which to perform the query. :param reuse_block: Whether to reuse the last-used block hash. - :return: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. + :return: The decoded result from the runtime API call, or ``None`` if the call fails. This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - call_definition = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] - - data = ( - "0x" - if params is None - else await self.encode_params( - call_definition=call_definition, params=params - ) - ) - api_method = f"{runtime_api}_{method}" - - json_result = await self.substrate.rpc_request( - method="state_call", - params=[api_method, data, block_hash] if block_hash else [api_method, data], + result = await self.substrate.runtime_call( + runtime_api, method, params, block_hash ) - if json_result is None: - return None - - return_type = call_definition["type"] - - as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) # type: ignore - - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None - - return obj.decode() + return result async def get_balance( + self, + address: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """ + Retrieves the balance for a single coldkey address + + :param address: coldkey address + :param block_hash: the block hash, optional + :param reuse_block: Whether to reuse the last-used block hash when retrieving info. + :return: Balance object representing the address's balance + """ + result = await self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + value = result or {"data": {"free": 0}} + return Balance(value["data"]["free"]) + + async def get_balances( self, *addresses: str, block_hash: Optional[str] = None, @@ -602,22 +582,17 @@ async def get_subnet_state( Returns: SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. """ - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if result is None: return None - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return SubnetState.from_vec_u8(bytes_result) + return SubnetState.from_any(result) async def get_hyperparameter( self, @@ -785,7 +760,7 @@ async def neurons_lite( 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. """ - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neurons_lite", params=[ @@ -795,15 +770,10 @@ async def neurons_lite( reuse_block=reuse_block, ) - if hex_bytes_result is None: + if result is None: return [] - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return NeuronInfoLite.list_from_vec_u8(bytes_result) + return NeuronInfoLite.list_from_any(result) async def neuron_for_uid( self, uid: Optional[int], netuid: int, block_hash: Optional[str] = None @@ -826,22 +796,20 @@ async def neuron_for_uid( if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neuron", - params=[netuid, uid], + params=[ + netuid, + uid, + ], # TODO check to see if this can accept more than one at a time block_hash=block_hash, ) - if not (result := hex_bytes_result): + if not result: return NeuronInfo.get_null_neuron() - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return NeuronInfo.from_vec_u8(bytes_result) + return NeuronInfo.from_any(result) async def get_delegated( self, @@ -868,24 +836,17 @@ async def get_delegated( if block_hash else (self.substrate.last_block_hash if reuse_block else None) ) - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="DelegateInfoRuntimeApi", method="get_delegated", - params=[encoded_coldkey], + params=[coldkey_ss58], block_hash=block_hash, ) - if not (result := hex_bytes_result): + if not result: return [] - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return DelegateInfo.delegated_list_from_vec_u8(bytes_result) + return DelegateInfo.list_from_any(result) async def query_all_identities( self, @@ -1183,22 +1144,28 @@ async def get_subnet_hyperparameters( 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. """ - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_hyperparams", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if not result: return [] - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) + return SubnetHyperparameters.from_any(result) - return SubnetHyperparameters.from_vec_u8(bytes_result) + async def burn_cost( + self, block_hash: Optional[str] = None + ) -> Optional[Balance]: + result = await self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block_hash=block_hash, + ) + return Balance.from_rao(result) if result is not None else None async def get_vote_data( self, @@ -1321,7 +1288,7 @@ async def get_delegates_by_netuid_light( if result in (None, []): return [] - return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet + return DelegateInfoLite.list_from_any(result) # TODO this won't work yet async def get_stake_for_coldkey_and_hotkey_on_netuid( self, @@ -1417,48 +1384,41 @@ async def get_stake_for_coldkeys( This function is useful for analyzing the stake distribution and delegation patterns of multiple accounts simultaneously, offering a broader perspective on network participation and investment strategies. """ - encoded_coldkeys = [ - ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list - ] - - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkeys", - params=[encoded_coldkeys], + params=[coldkey_ss58_list], block_hash=block_hash, ) - - if hex_bytes_result is None: + if result is None: return None - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + stake_info_map = {} + for coldkey_bytes, stake_info_list in result: + coldkey_ss58 = decode_account_id(coldkey_bytes) + stake_info_map[coldkey_ss58] = StakeInfo.list_from_any(stake_info_list) + return stake_info_map async def all_subnets( self, block_hash: Optional[str] = None ) -> list["DynamicInfo"]: - query = await self.substrate.runtime_call( + result = await self.substrate.runtime_call( "SubnetInfoRuntimeApi", "get_all_dynamic_info", block_hash=block_hash, ) - subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) - return subnets + return DynamicInfo.list_from_any(result) async def subnet( self, netuid: int, block_hash: Optional[str] = None ) -> "DynamicInfo": - query = await self.substrate.runtime_call( + result = await self.substrate.runtime_call( "SubnetInfoRuntimeApi", "get_dynamic_info", params=[netuid], block_hash=block_hash, ) - return DynamicInfo.from_vec_u8(bytes.fromhex(query.decode()[2:])) + return DynamicInfo.from_any(result) async def get_owned_hotkeys( self, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 08cfd9e0..b86822d6 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -15,6 +15,7 @@ from bittensor_wallet.utils import SS58_FORMAT from bittensor_wallet.errors import KeyFileError from bittensor_wallet import utils +import bt_decode from jinja2 import Template from markupsafe import Markup import numpy as np @@ -24,6 +25,7 @@ import scalecodec from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset +from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -376,21 +378,54 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_scale_bytes(return_type, scale_bytes, custom_rpc_type_registry): - """Decodes a ScaleBytes object using our type registry and return type""" - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - obj = rpc_runtime_config.create_scale_object(return_type, scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result +def decode_scale_bytes( + return_type: str, + scale_bytes: Union["scalecodec.ScaleBytes", bytes], + custom_rpc_type_registry: Union[str, "bt_decode.PortableRegistry"], +) -> Any: + """ + Decodes a ScaleBytes object using our type registry and return type + + :param return_type: the type string to decode the scale bytes to + :param scale_bytes: the scale bytes to decode (either a scalecodec.ScaleBytes or bytes) + :param custom_rpc_type_registry: contains the type registry + + :return: the decoded object + """ + if isinstance(custom_rpc_type_registry, str): + portable_registry = bt_decode.PortableRegistry.from_json( + custom_rpc_type_registry + ) + else: + portable_registry = custom_rpc_type_registry + + if isinstance(scale_bytes, scalecodec.ScaleBytes): + as_bytes = bytes(scale_bytes.data) + else: + as_bytes = bytes(scale_bytes) + + if as_bytes.hex() == "0x0400": # RPC returned None result return None - return obj.decode() + + return bt_decode.decode(return_type, portable_registry, as_bytes) + + +def bytes_from_hex_string_result(hex_string_result: str) -> bytes: + if hex_string_result.startswith("0x"): + hex_string_result = hex_string_result[2:] + + return bytes.fromhex(hex_string_result) + + +def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]): + if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): + account_id_bytes = account_id_bytes[0] + # Convert the AccountId bytes to a Base64 string + return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) -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, SS58_FORMAT) - return bytes.fromhex(account_id_hex) +def encode_account_id(ss58_address: str) -> bytes: + return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT)) def ss58_to_vec_u8(ss58_address: str) -> list[int]: @@ -401,7 +436,7 @@ def ss58_to_vec_u8(ss58_address: str) -> list[int]: :return: A list of integers representing the byte values of the SS58 address. """ - ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) + ss58_bytes: bytes = encode_account_id(ss58_address) encoded_address: list[int] = [int(byte) for byte in ss58_bytes] return encoded_address @@ -489,7 +524,7 @@ def format_error_message( elif all(x in d for x in ["code", "message", "data"]): new_error_message = d break - except ValueError: + except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError): pass if new_error_message is None: return_val = " ".join(error_message.args) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index bd95a0e5..6ec589d4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -91,9 +91,7 @@ async def stake_add( current_wallet_balance_ = await subtensor.get_balance( wallet.coldkeypub.ss58_address ) - current_wallet_balance = current_wallet_balance_[ - wallet.coldkeypub.ss58_address - ].set_unit(0) + current_wallet_balance = current_wallet_balance_.set_unit(0) remaining_wallet_balance = current_wallet_balance max_slippage = 0.0 @@ -140,8 +138,8 @@ async def stake_add( starting_chain_head = await subtensor.substrate.get_chain_head() _all_dynamic_info, stake_info_dict = await asyncio.gather( subtensor.all_subnets(), - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=starting_chain_head, ), ) @@ -152,7 +150,7 @@ async def stake_add( for netuid in netuids: initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) - for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + for stake_info in stake_info_dict: if stake_info.hotkey_ss58 in initial_stake_balances: initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = ( stake_info.stake @@ -315,15 +313,14 @@ async def send_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: - new_balance_, stake_info_dict = await asyncio.gather( + new_balance, stake_info_dict = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, ), ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] new_stake = Balance.from_rao(0) - for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + for stake_info in stake_info_dict: if ( stake_info.hotkey_ss58 == staking_address_ss58 and stake_info.netuid == netuid_i @@ -759,10 +756,9 @@ async def _unstake_all( else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" ) console.print(success_message) - new_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) console.print( - f"Balance:\n [blue]{current_wallet_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) return True else: @@ -875,28 +871,24 @@ async def unstake( # Prepare unstaking transactions unstake_operations = [] total_received_amount = Balance.from_tao(0) - current_wallet_balance: Balance = ( - await subtensor.get_balance(wallet.coldkeypub.ss58_address) - )[wallet.coldkeypub.ss58_address] + current_wallet_balance: Balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) max_float_slippage = 0 # Fetch stake balances chain_head = await subtensor.substrate.get_chain_head() - stake_info_dict = await subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + stake_info_list = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=chain_head, ) stake_in_netuids = {} - for _, stake_info_list in stake_info_dict.items(): - hotkey_stakes = {} - for stake_info in stake_info_list: - if stake_info.hotkey_ss58 not in hotkey_stakes: - hotkey_stakes[stake_info.hotkey_ss58] = {} - hotkey_stakes[stake_info.hotkey_ss58][stake_info.netuid] = ( - stake_info.stake - ) - - stake_in_netuids = hotkey_stakes + for stake_info in stake_info_list: + if stake_info.hotkey_ss58 not in stake_in_netuids: + stake_in_netuids[stake_info.hotkey_ss58] = {} + stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) # Flag to check if user wants to quit skip_remaining_subnets = False @@ -1105,10 +1097,9 @@ async def unstake( status, ) else: - new_balance_ = await subtensor.get_balance( + new_balance = await subtensor.get_balance( wallet.coldkeypub.ss58_address ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) @@ -1150,17 +1141,14 @@ async def unstake( status, ) else: - new_balance_ = await subtensor.get_balance( + new_balance = await subtensor.get_balance( wallet.coldkeypub.ss58_address ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake_info = await subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + new_stake_info = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, ) new_stake = Balance.from_rao(0) - for stake_info in new_stake_info[ - wallet.coldkeypub.ss58_address - ]: + for stake_info in new_stake_info: if ( stake_info.hotkey_ss58 == staking_address_ss58 and stake_info.netuid == netuid_i @@ -1190,17 +1178,17 @@ async def stake_list( async def get_stake_data(block_hash: str = None): ( - substakes, + sub_stakes, registered_delegate_info, _dynamic_info, ) = await asyncio.gather( - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[coldkey_address], block_hash=block_hash + subtensor.get_stake_for_coldkey( + coldkey_ss58=coldkey_address, block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.all_subnets(), ) - sub_stakes = substakes[coldkey_address] + # sub_stakes = substakes[coldkey_address] dynamic_info = {info.netuid: info for info in _dynamic_info} return ( sub_stakes, @@ -1748,7 +1736,7 @@ def format_cell( console.print( f"Wallet:\n" f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance[coldkey_address]}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_ownership}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 88fb63ec..51ec34f5 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -91,8 +91,7 @@ async def _find_event_attributes_in_extrinsic_receipt( return [-1] print_verbose("Fetching balance") - your_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - your_balance = your_balance_[wallet.coldkeypub.ss58_address] + your_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) print_verbose("Fetching burn_cost") sn_burn_cost = await burn_cost(subtensor) @@ -1344,20 +1343,17 @@ async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: f":satellite:Retrieving lock cost from {subtensor.network}...", spinner="aesthetic", ): - lc = await subtensor.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - ) - if lc: - burn_cost_ = Balance(lc) - console.print( - f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost_}" - ) - return burn_cost_ - else: - err_console.print("Subnet burn cost: [red]Failed to get subnet burn cost[/red]") - return None + burn_cost = await subtensor.burn_cost() + if burn_cost: + console.print( + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost}" + ) + return burn_cost + else: + err_console.print( + "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" + ) + return None async def create( @@ -1453,7 +1449,7 @@ async def register( # Check current recycle amount print_verbose("Fetching recycle amount") - current_recycle_, balance_ = await asyncio.gather( + current_recycle_, balance = await asyncio.gather( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid, block_hash=block_hash ), @@ -1462,7 +1458,6 @@ async def register( current_recycle = ( Balance.from_rao(int(current_recycle_)) if current_recycle_ else Balance(0) ) - balance = balance_[wallet.coldkeypub.ss58_address] # Check balance is sufficient if balance < current_recycle: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c2b8e021..f0cbfc43 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -288,7 +288,7 @@ async def wallet_balance( block_hash = await subtensor.substrate.get_chain_head() free_balances, staked_balances = await asyncio.gather( - subtensor.get_balance(*coldkeys, block_hash=block_hash), + subtensor.get_balances(*coldkeys, block_hash=block_hash), subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), ) @@ -552,7 +552,7 @@ async def _get_total_balance( ] total_balance += sum( ( - await subtensor.get_balance( + await subtensor.get_balances( *(x.coldkeypub.ss58_address for x in _balance_cold_wallets), block_hash=block_hash, ) @@ -574,7 +574,7 @@ async def _get_total_balance( ): total_balance = sum( ( - await subtensor.get_balance( + await subtensor.get_balances( coldkey_wallet.coldkeypub.ss58_address, block_hash=block_hash ) ).values() @@ -1038,19 +1038,7 @@ async def _fetch_neuron_for_netuid( :return: the original netuid, and a mapping of the neurons to their NeuronInfoLite objects """ - - async def neurons_lite_for_uid(uid: int) -> dict[Any, Any]: - block_hash = subtensor.substrate.last_block_hash - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[uid], - block_hash=block_hash, - ) - - return hex_bytes_result - - neurons = await neurons_lite_for_uid(uid=netuid) + neurons = await subtensor.neurons_lite(netuid=netuid) return netuid, neurons @@ -1075,7 +1063,7 @@ def _process_neurons_for_netuids( :return: netuids mapped to decoded neurons """ all_results = [ - (netuid, NeuronInfoLite.list_from_vec_u8(bytes.fromhex(result[2:]))) + (netuid, NeuronInfoLite.list_from_any(bytes.fromhex(result[2:]))) for netuid, result in netuids_with_all_neurons_hex_bytes ] return all_results @@ -1211,7 +1199,7 @@ def neuron_row_maker( all_delegates: list[list[tuple[DelegateInfo, Balance]]] with console.status("Pulling balance data...", spinner="aesthetic"): balances, all_neurons, all_delegates = await asyncio.gather( - subtensor.get_balance( + subtensor.get_balances( *[w.coldkeypub.ss58_address for w in wallets_with_ckp_file], block_hash=block_hash, ), diff --git a/requirements.txt b/requirements.txt index c98676e6..8f59d944 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 bittensor-wallet>=2.0.2 -bt-decode==0.4.0 +bt-decode==v0.5.0-a0 plotille pywry plotly From 63546d55bb448a0cf15e149226721fd111474885 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 28 Jan 2025 09:59:01 -0800 Subject: [PATCH 233/332] Removes redundant methods --- .../bittensor/async_substrate_interface.py | 85 ------------------- 1 file changed, 85 deletions(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index e9419c07..c919fd57 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -51,33 +51,6 @@ def timeout_handler(signum, frame): raise TimeoutException("Operation timed out") -class DictWithValue(dict): - value: Any - - def __init__(self, value: Any = None): - super().__init__() - self.value = value - - def __getitem__(self, key: Union[str, int]): - result = super().get(key) - if not result and isinstance(key, int): - # if the key is not found, return the key at the given index - return list(chain.from_iterable(self.items()))[key] - return result - - @classmethod - def from_dict(cls, dict_: dict): - inst = cls() - # recursively convert all values to DictWithValue - for key, value in dict_.items(): - if isinstance(value, dict): - value = cls.from_dict(value) - inst[key] = value - inst.value = dict_ - - return inst - - class ExtrinsicReceipt: """ Object containing information of submitted extrinsic. Block hash where extrinsic is included is required @@ -863,64 +836,6 @@ async def initialize(self): async def __aexit__(self, exc_type, exc_val, exc_tb): pass - @staticmethod - def _type_registry_to_scale_info_types( - registry_types: list[dict[str, Any]], - ) -> list[dict[str, Any]]: - scale_info_types = [] - for type_entry in registry_types: - new_type_entry = DictWithValue(value=type_entry) - if ( - "variant" in type_entry["type"]["def"] - and len(type_entry["type"]["def"]["variant"]) == 0 - ): - type_entry["type"]["def"]["variant"] = { - "variants": [] - } # add empty variants field to variant type if empty - - for key, value in type_entry.items(): - if isinstance(value, dict): - entry = DictWithValue.from_dict(value) - else: - entry = SimpleNamespace(value=value) - new_type_entry[key] = entry - - scale_info_types.append(new_type_entry) - - return scale_info_types - - @staticmethod - def _type_id_to_name(ty_id: int) -> str: - type_string = f"scale_info::{ty_id}" - - return type_string - - def _type_registry_apis_to_runtime_api( - self, apis: list[dict[str, Any]] - ) -> dict[str, Any]: - runtime_api = {} - for api in apis: - api_name = api["name"] - methods = api["methods"] - - runtime_api[api_name] = { - "methods": { - method["name"]: { - "description": "\n".join(method["docs"]), - "params": [ - { - "name": input["name"], - "type": self._type_id_to_name(input["ty"]), - } - for input in method["inputs"] - ], - "type": self._type_id_to_name(method["output"]), - } - for method in methods - } - } - return runtime_api - @property def chain(self): """ From 50572ee3f0b87c40ec33891ad3b3e4b0d730ba31 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 29 Jan 2025 16:55:43 +0200 Subject: [PATCH 234/332] Allow display of basic information about encrypted hotkeys in wallets in `btcli w list` --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/bittensor/utils.py | 40 +++++++++++++++++++----- bittensor_cli/src/commands/wallets.py | 12 +++---- tests/e2e_tests/test_wallet_creations.py | 4 +-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5f3dfa59..b020e1ba 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1252,7 +1252,7 @@ def wallet_list( """ Displays all the wallets and their corresponding hotkeys that are located in the wallet path specified in the config. - The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output is presented in a hierarchical tree format, with each wallet as a root node and any associated hotkeys as child nodes. The `ss58` address is displayed for each coldkey and hotkey that is not encrypted and exists on the device. + The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output is presented in a hierarchical tree format, with each wallet as a root node and any associated hotkeys as child nodes. The `ss58` address (or an `` marker, for encrypted hotkeys) is displayed for each coldkey and hotkey that exists on the device. Upon invocation, the command scans the wallet directory and prints a list of all the wallets, indicating whether the public keys are available (`?` denotes unavailable or encrypted keys). diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 71d5e12a..2463c618 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -28,9 +28,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters - from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - ) + console = Console() err_console = Console(stderr=True) @@ -39,6 +37,23 @@ UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) +class _Hotkey: + def __init__(self, hotkey_ss58=None): + self.ss58_address = hotkey_ss58 + + +class WalletLike: + def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None): + self.name = name + self.hotkey_ss58 = hotkey_ss58 + self.hotkey_str = hotkey_str + self._hotkey = _Hotkey(hotkey_ss58) + + @property + def hotkey(self): + return self._hotkey + + def print_console(message: str, colour: str, title: str, console: Console): console.print( f"[bold {colour}][{title}]:[/bold {colour}] [{colour}]{message}[/{colour}]\n" @@ -197,13 +212,14 @@ def convert_root_weight_uids_and_vals_to_tensor( def get_hotkey_wallets_for_wallet( - wallet: Wallet, show_nulls: bool = False + wallet: Wallet, show_nulls: bool = False, show_encrypted: bool = False ) -> list[Optional[Wallet]]: """ Returns wallet objects with hotkeys for a single given wallet :param wallet: Wallet object to use for the path :param show_nulls: will add `None` into the output if a hotkey is encrypted or not on the device + :param show_encrypted: will add some basic info about the encrypted hotkey :return: a list of wallets (with Nones included for cases of a hotkey being encrypted or not on the device, if `show_nulls` is set to `True`) @@ -219,12 +235,18 @@ def get_hotkey_wallets_for_wallet( hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name) try: if ( - hotkey_for_name.hotkey_file.exists_on_device() + exists := hotkey_for_name.hotkey_file.exists_on_device() and not hotkey_for_name.hotkey_file.is_encrypted() # and hotkey_for_name.coldkeypub.ss58_address and hotkey_for_name.hotkey.ss58_address ): hotkey_wallets.append(hotkey_for_name) + elif ( + show_encrypted and exists and hotkey_for_name.hotkey_file.is_encrypted() + ): + hotkey_wallets.append( + WalletLike(str(wallet_path), "", h_name) + ) elif show_nulls: hotkey_wallets.append(None) except ( @@ -507,7 +529,10 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: # subtensor custom error marker if err_data.startswith("Custom error:"): - err_description = f"{err_data} | Please consult https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages" + err_description = ( + f"{err_data} | Please consult " + f"https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages" + ) else: err_description = err_data @@ -542,7 +567,8 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]: """ Decodes hex-encoded strings in a dictionary. - This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable strings. It handles nested dictionaries and lists within the dictionary. + This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable + strings. It handles nested dictionaries and lists within the dictionary. Args: info_dictionary (dict): The dictionary containing hex-encoded strings to decode. diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index d49c03e2..9b55b4e5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -52,16 +52,10 @@ retry_prompt, unlock_key, hex_to_bytes, + WalletLike, ) -class WalletLike: - def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None): - self.name = name - self.hotkey_ss58 = hotkey_ss58 - self.hotkey_str = hotkey_str - - async def regen_coldkey( wallet: Wallet, mnemonic: Optional[str], @@ -497,7 +491,9 @@ async def wallet_list(wallet_path: str): wallet_tree = root.add( f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]" ) - hotkeys = utils.get_hotkey_wallets_for_wallet(wallet, show_nulls=True) + hotkeys = utils.get_hotkey_wallets_for_wallet( + wallet, show_nulls=True, show_encrypted=True + ) for hkey in hotkeys: data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" if hkey: diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 7490a480..52aa3139 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -373,7 +373,7 @@ def test_wallet_regen(wallet_setup, capfd): "--mnemonic", mnemonics["coldkey"], "--no-use-password", - "--overwrite" + "--overwrite", ], ) @@ -413,7 +413,7 @@ def test_wallet_regen(wallet_setup, capfd): wallet_path, "--ss58-address", ss58_address, - "--overwrite" + "--overwrite", ], ) From d2595f6ba9959ebac0fd0ca365f022ddd116a0b3 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 29 Jan 2025 17:12:00 +0200 Subject: [PATCH 235/332] Walrus operator --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 2463c618..82f22972 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -235,7 +235,7 @@ def get_hotkey_wallets_for_wallet( hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name) try: if ( - exists := hotkey_for_name.hotkey_file.exists_on_device() + (exists := hotkey_for_name.hotkey_file.exists_on_device()) and not hotkey_for_name.hotkey_file.is_encrypted() # and hotkey_for_name.coldkeypub.ss58_address and hotkey_for_name.hotkey.ss58_address From 8673ff5f9524af6b47f9b4b718aa9abbb0cc4d5b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 00:16:23 +0200 Subject: [PATCH 236/332] Remove async_substrate_interface.py, cleanup started --- bittensor_cli/cli.py | 4 +- .../bittensor/async_substrate_interface.py | 2829 ----------------- .../src/bittensor/extrinsics/transfer.py | 6 +- .../src/bittensor/subtensor_interface.py | 267 +- bittensor_cli/src/bittensor/utils.py | 4 +- requirements.txt | 1 + tests/e2e_tests/conftest.py | 4 +- tests/e2e_tests/utils.py | 4 +- 8 files changed, 165 insertions(+), 2954 deletions(-) delete mode 100644 bittensor_cli/src/bittensor/async_substrate_interface.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7bd37fbb..01e29b3a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -26,9 +26,7 @@ ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.async_substrate_interface import ( - SubstrateRequestException, -) +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.commands import sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py deleted file mode 100644 index c919fd57..00000000 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ /dev/null @@ -1,2829 +0,0 @@ -import asyncio -import json -import random -from collections import defaultdict -from dataclasses import dataclass -from hashlib import blake2b -from itertools import chain -from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING -from types import SimpleNamespace - -from bt_decode import ( - PortableRegistry, - decode as decode_by_type_string, - encode as encode_by_type_string, - MetadataV15, -) - - -from async_property import async_property -from scalecodec import GenericExtrinsic -from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall -from bittensor_wallet import Keypair -from substrateinterface.exceptions import ( - SubstrateRequestException, - ExtrinsicNotFound, - BlockNotFound, -) -from substrateinterface.storage import StorageKey -from websockets.asyncio.client import connect -from websockets.exceptions import ConnectionClosed - -from .utils import ( - bytes_from_hex_string_result, - encode_account_id, - decode_account_id, -) - -if TYPE_CHECKING: - from websockets.asyncio.client import ClientConnection - -ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]] - - -class TimeoutException(Exception): - pass - - -def timeout_handler(signum, frame): - raise TimeoutException("Operation timed out") - - -class ExtrinsicReceipt: - """ - Object containing information of submitted extrinsic. Block hash where extrinsic is included is required - when retrieving triggered events or determine if extrinsic was successful - """ - - def __init__( - self, - substrate: "AsyncSubstrateInterface", - extrinsic_hash: Optional[str] = None, - block_hash: Optional[str] = None, - block_number: Optional[int] = None, - extrinsic_idx: Optional[int] = None, - finalized=None, - ): - """ - Object containing information of submitted extrinsic. Block hash where extrinsic is included is required - when retrieving triggered events or determine if extrinsic was successful - - Parameters - ---------- - substrate - extrinsic_hash - block_hash - finalized - """ - self.substrate = substrate - self.extrinsic_hash = extrinsic_hash - self.block_hash = block_hash - self.block_number = block_number - self.finalized = finalized - - self.__extrinsic_idx = extrinsic_idx - self.__extrinsic = None - - self.__triggered_events: Optional[list] = None - self.__is_success: Optional[bool] = None - self.__error_message = None - self.__weight = None - self.__total_fee_amount = None - - async def get_extrinsic_identifier(self) -> str: - """ - Returns the on-chain identifier for this extrinsic in format "[block_number]-[extrinsic_idx]" e.g. 134324-2 - Returns - ------- - str - """ - if self.block_number is None: - if self.block_hash is None: - raise ValueError( - "Cannot create extrinsic identifier: block_hash is not set" - ) - - self.block_number = await self.substrate.get_block_number(self.block_hash) - - if self.block_number is None: - raise ValueError( - "Cannot create extrinsic identifier: unknown block_hash" - ) - - return f"{self.block_number}-{await self.extrinsic_idx}" - - async def retrieve_extrinsic(self): - if not self.block_hash: - raise ValueError( - "ExtrinsicReceipt can't retrieve events because it's unknown which block_hash it is " - "included, manually set block_hash or use `wait_for_inclusion` when sending extrinsic" - ) - # Determine extrinsic idx - - block = await self.substrate.get_block(block_hash=self.block_hash) - - extrinsics = block["extrinsics"] - - if len(extrinsics) > 0: - if self.__extrinsic_idx is None: - self.__extrinsic_idx = self.__get_extrinsic_index( - block_extrinsics=extrinsics, extrinsic_hash=self.extrinsic_hash - ) - - if self.__extrinsic_idx >= len(extrinsics): - raise ExtrinsicNotFound() - - self.__extrinsic = extrinsics[self.__extrinsic_idx] - - @async_property - async def extrinsic_idx(self) -> int: - """ - Retrieves the index of this extrinsic in containing block - - Returns - ------- - int - """ - if self.__extrinsic_idx is None: - await self.retrieve_extrinsic() - return self.__extrinsic_idx - - @async_property - async def triggered_events(self) -> list: - """ - Gets triggered events for submitted extrinsic. block_hash where extrinsic is included is required, manually - set block_hash or use `wait_for_inclusion` when submitting extrinsic - - Returns - ------- - list - """ - if self.__triggered_events is None: - if not self.block_hash: - raise ValueError( - "ExtrinsicReceipt can't retrieve events because it's unknown which block_hash it is " - "included, manually set block_hash or use `wait_for_inclusion` when sending extrinsic" - ) - - if await self.extrinsic_idx is None: - await self.retrieve_extrinsic() - - self.__triggered_events = [] - - for event in await self.substrate.get_events(block_hash=self.block_hash): - if event["extrinsic_idx"] == await self.extrinsic_idx: - self.__triggered_events.append(event) - - return cast(list, self.__triggered_events) - - async def process_events(self): - if await self.triggered_events: - self.__total_fee_amount = 0 - - # Process fees - has_transaction_fee_paid_event = False - - for event in await self.triggered_events: - if ( - event["event"]["module_id"] == "TransactionPayment" - and event["event"]["event_id"] == "TransactionFeePaid" - ): - self.__total_fee_amount = event["event"]["attributes"]["actual_fee"] - has_transaction_fee_paid_event = True - - # Process other events - for event in await self.triggered_events: - # Check events - if ( - event["event"]["module_id"] == "System" - and event["event"]["event_id"] == "ExtrinsicSuccess" - ): - self.__is_success = True - self.__error_message = None - - if "dispatch_info" in event["event"]["attributes"]: - self.__weight = event["event"]["attributes"]["dispatch_info"][ - "weight" - ] - else: - # Backwards compatibility - self.__weight = event["event"]["attributes"]["weight"] - - elif ( - event["event"]["module_id"] == "System" - and event["event"]["event_id"] == "ExtrinsicFailed" - ): - self.__is_success = False - - dispatch_info = event["event"]["attributes"]["dispatch_info"] - dispatch_error = event["event"]["attributes"]["dispatch_error"] - - self.__weight = dispatch_info["weight"] - - if "Module" in dispatch_error: - module_index = dispatch_error["Module"][0]["index"] - error_index = int.from_bytes( - bytes(dispatch_error["Module"][0]["error"]), - byteorder="little", - signed=False, - ) - - if isinstance(error_index, str): - # Actual error index is first u8 in new [u8; 4] format - error_index = int(error_index[2:4], 16) - module_error = self.substrate.metadata.get_module_error( - module_index=module_index, error_index=error_index - ) - self.__error_message = { - "type": "Module", - "name": module_error.name, - "docs": module_error.docs, - } - elif "BadOrigin" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "BadOrigin", - "docs": "Bad origin", - } - elif "CannotLookup" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "CannotLookup", - "docs": "Cannot lookup", - } - elif "Other" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "Other", - "docs": "Unspecified error occurred", - } - - elif not has_transaction_fee_paid_event: - if ( - event["event"]["module_id"] == "Treasury" - and event["event"]["event_id"] == "Deposit" - ): - self.__total_fee_amount += event["event"]["attributes"]["value"] - elif ( - event["event"]["module_id"] == "Balances" - and event["event"]["event_id"] == "Deposit" - ): - self.__total_fee_amount += event.value["attributes"]["amount"] - - @async_property - async def is_success(self) -> bool: - """ - Returns `True` if `ExtrinsicSuccess` event is triggered, `False` in case of `ExtrinsicFailed` - In case of False `error_message` will contain more details about the error - - - Returns - ------- - bool - """ - if self.__is_success is None: - await self.process_events() - - return cast(bool, self.__is_success) - - @async_property - async def error_message(self) -> Optional[dict]: - """ - Returns the error message if the extrinsic failed in format e.g.: - - `{'type': 'System', 'name': 'BadOrigin', 'docs': 'Bad origin'}` - - Returns - ------- - dict - """ - if self.__error_message is None: - if await self.is_success: - return None - await self.process_events() - return self.__error_message - - @async_property - async def weight(self) -> Union[int, dict]: - """ - Contains the actual weight when executing this extrinsic - - Returns - ------- - int (WeightV1) or dict (WeightV2) - """ - if self.__weight is None: - await self.process_events() - return self.__weight - - @async_property - async def total_fee_amount(self) -> int: - """ - Contains the total fee costs deducted when executing this extrinsic. This includes fee for the validator ( - (`Balances.Deposit` event) and the fee deposited for the treasury (`Treasury.Deposit` event) - - Returns - ------- - int - """ - if self.__total_fee_amount is None: - await self.process_events() - return cast(int, self.__total_fee_amount) - - # Helper functions - @staticmethod - def __get_extrinsic_index(block_extrinsics: list, extrinsic_hash: str) -> int: - """ - Returns the index of a provided extrinsic - """ - for idx, extrinsic in enumerate(block_extrinsics): - if ( - extrinsic.extrinsic_hash - and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash - ): - return idx - raise ExtrinsicNotFound() - - # Backwards compatibility methods - def __getitem__(self, item): - return getattr(self, item) - - def __iter__(self): - for item in self.__dict__.items(): - yield item - - def get(self, name): - return self[name] - - -class QueryMapResult: - def __init__( - self, - records: list, - page_size: int, - substrate: "AsyncSubstrateInterface", - module: Optional[str] = None, - storage_function: Optional[str] = None, - params: Optional[list] = None, - block_hash: Optional[str] = None, - last_key: Optional[str] = None, - max_results: Optional[int] = None, - ignore_decoding_errors: bool = False, - ): - self.records = records - self.page_size = page_size - self.module = module - self.storage_function = storage_function - self.block_hash = block_hash - self.substrate = substrate - self.last_key = last_key - self.max_results = max_results - self.params = params - self.ignore_decoding_errors = ignore_decoding_errors - self.loading_complete = False - self._buffer = iter(self.records) # Initialize the buffer with initial records - - async def retrieve_next_page(self, start_key) -> list: - result = await self.substrate.query_map( - module=self.module, - storage_function=self.storage_function, - params=self.params, - page_size=self.page_size, - block_hash=self.block_hash, - start_key=start_key, - max_results=self.max_results, - ignore_decoding_errors=self.ignore_decoding_errors, - ) - - # Update last key from new result set to use as offset for next page - self.last_key = result.last_key - return result.records - - def __aiter__(self): - return self - - async def __anext__(self): - try: - # Try to get the next record from the buffer - return next(self._buffer) - except StopIteration: - # If no more records in the buffer, try to fetch the next page - if self.loading_complete: - raise StopAsyncIteration - - next_page = await self.retrieve_next_page(self.last_key) - if not next_page: - self.loading_complete = True - raise StopAsyncIteration - - # Update the buffer with the newly fetched records - self._buffer = iter(next_page) - return next(self._buffer) - - def __getitem__(self, item): - return self.records[item] - - -@dataclass -class Preprocessed: - queryable: str - method: str - params: list - value_scale_type: str - storage_item: ScaleType - - -class RuntimeCache: - blocks: dict[int, "Runtime"] - block_hashes: dict[str, "Runtime"] - - def __init__(self): - self.blocks = {} - self.block_hashes = {} - - def add_item( - self, block: Optional[int], block_hash: Optional[str], runtime: "Runtime" - ): - if block is not None: - self.blocks[block] = runtime - if block_hash is not None: - self.block_hashes[block_hash] = runtime - - def retrieve( - self, block: Optional[int] = None, block_hash: Optional[str] = None - ) -> Optional["Runtime"]: - if block is not None: - return self.blocks.get(block) - elif block_hash is not None: - return self.block_hashes.get(block_hash) - else: - return None - - -class Runtime: - block_hash: str - block_id: int - runtime_version = None - transaction_version = None - cache_region = None - metadata = None - type_registry_preset = None - - def __init__(self, chain, runtime_config, metadata, type_registry): - self.runtime_config = RuntimeConfigurationObject() - self.config = {} - self.chain = chain - self.type_registry = type_registry - self.runtime_config = runtime_config - self.metadata = metadata - - def __str__(self): - return f"Runtime: {self.chain} | {self.config}" - - @property - def implements_scaleinfo(self) -> bool: - """ - Returns True if current runtime implementation a `PortableRegistry` (`MetadataV14` and higher) - """ - if self.metadata: - return self.metadata.portable_registry is not None - else: - return False - - def reload_type_registry( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - """ - Reload type registry and preset used to instantiate the SubstrateInterface object. Useful to periodically apply - changes in type definitions when a runtime upgrade occurred - - Parameters - ---------- - use_remote_preset: When True preset is downloaded from Github master, otherwise use files from local installed - scalecodec package - auto_discover - - Returns - ------- - - """ - self.runtime_config.clear_type_registry() - - self.runtime_config.implements_scale_info = self.implements_scaleinfo - - # Load metadata types in runtime configuration - self.runtime_config.update_type_registry(load_type_registry_preset(name="core")) - self.apply_type_registry_presets( - use_remote_preset=use_remote_preset, auto_discover=auto_discover - ) - - def apply_type_registry_presets( - self, - use_remote_preset: bool = True, - auto_discover: bool = True, - ): - """ - Applies type registry presets to the runtime - :param use_remote_preset: bool, whether to use presets from remote - :param auto_discover: bool, whether to use presets from local installed scalecodec package - """ - if self.type_registry_preset is not None: - # Load type registry according to preset - type_registry_preset_dict = load_type_registry_preset( - name=self.type_registry_preset, use_remote_preset=use_remote_preset - ) - - if not type_registry_preset_dict: - raise ValueError( - f"Type registry preset '{self.type_registry_preset}' not found" - ) - - elif auto_discover: - # Try to auto discover type registry preset by chain name - type_registry_name = self.chain.lower().replace(" ", "-") - try: - type_registry_preset_dict = load_type_registry_preset( - type_registry_name - ) - self.type_registry_preset = type_registry_name - except ValueError: - type_registry_preset_dict = None - - else: - type_registry_preset_dict = None - - if type_registry_preset_dict: - # Load type registries in runtime configuration - if self.implements_scaleinfo is False: - # Only runtime with no embedded types in metadata need the default set of explicit defined types - self.runtime_config.update_type_registry( - load_type_registry_preset( - "legacy", use_remote_preset=use_remote_preset - ) - ) - - if self.type_registry_preset != "legacy": - self.runtime_config.update_type_registry(type_registry_preset_dict) - - if self.type_registry: - # Load type registries in runtime configuration - self.runtime_config.update_type_registry(self.type_registry) - - -class RequestManager: - RequestResults = dict[Union[str, int], list[Union[ScaleType, dict]]] - - def __init__(self, payloads): - self.response_map = {} - self.responses = defaultdict(lambda: {"complete": False, "results": []}) - self.payloads_count = len(payloads) - - def add_request(self, item_id: int, request_id: Any): - """ - Adds an outgoing request to the responses map for later retrieval - """ - self.response_map[item_id] = request_id - - def overwrite_request(self, item_id: int, request_id: Any): - """ - Overwrites an existing request in the responses map with a new request_id. This is used - for multipart responses that generate a subscription id we need to watch, rather than the initial - request_id. - """ - self.response_map[request_id] = self.response_map.pop(item_id) - return request_id - - def add_response(self, item_id: int, response: dict, complete: bool): - """ - Maps a response to the request for later retrieval - """ - request_id = self.response_map[item_id] - self.responses[request_id]["results"].append(response) - self.responses[request_id]["complete"] = complete - - @property - def is_complete(self) -> bool: - """ - Returns whether all requests in the manager have completed - """ - return ( - all(info["complete"] for info in self.responses.values()) - and len(self.responses) == self.payloads_count - ) - - def get_results(self) -> RequestResults: - """ - Generates a dictionary mapping the requests initiated to the responses received. - """ - return { - request_id: info["results"] for request_id, info in self.responses.items() - } - - -class Websocket: - def __init__( - self, - ws_url: str, - max_subscriptions=1024, - max_connections=100, - shutdown_timer=5, - options: Optional[dict] = None, - ): - """ - Websocket manager object. Allows for the use of a single websocket connection by multiple - calls. - - :param ws_url: Websocket URL to connect to - :param max_subscriptions: Maximum number of subscriptions per websocket connection - :param max_connections: Maximum number of connections total - :param shutdown_timer: Number of seconds to shut down websocket connection after last use - """ - # TODO allow setting max concurrent connections and rpc subscriptions per connection - # TODO reconnection logic - self.ws_url = ws_url - self.ws: Optional["ClientConnection"] = None - self.id = 0 - self.max_subscriptions = max_subscriptions - self.max_connections = max_connections - self.shutdown_timer = shutdown_timer - self._received = {} - self._in_use = 0 - self._receiving_task = None - self._attempts = 0 - self._initialized = False - self._lock = asyncio.Lock() - self._exit_task = None - self._open_subscriptions = 0 - self._options = options if options else {} - - async def __aenter__(self): - async with self._lock: - self._in_use += 1 - if self._exit_task: - self._exit_task.cancel() - if not self._initialized: - self._initialized = True - self.ws = await asyncio.wait_for( - connect(self.ws_url, **self._options), timeout=10 - ) - self._receiving_task = asyncio.create_task(self._start_receiving()) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - async with self._lock: - self._in_use -= 1 - if self._exit_task is not None: - self._exit_task.cancel() - try: - await self._exit_task - except asyncio.CancelledError: - pass - if self._in_use == 0 and self.ws is not None: - self.id = 0 - self._open_subscriptions = 0 - self._exit_task = asyncio.create_task(self._exit_with_timer()) - - async def _exit_with_timer(self): - """ - Allows for graceful shutdown of websocket connection after specified number of seconds, allowing - for reuse of the websocket connection. - """ - try: - await asyncio.sleep(self.shutdown_timer) - await self.shutdown() - except asyncio.CancelledError: - pass - - async def shutdown(self): - async with self._lock: - try: - self._receiving_task.cancel() - await self._receiving_task - await self.ws.close() - except (AttributeError, asyncio.CancelledError): - pass - self.ws = None - self._initialized = False - self._receiving_task = None - self.id = 0 - - async def _recv(self) -> None: - try: - response = json.loads(await self.ws.recv()) - async with self._lock: - self._open_subscriptions -= 1 - if "id" in response: - self._received[response["id"]] = response - elif "params" in response: - self._received[response["params"]["subscription"]] = response - else: - raise KeyError(response) - except ConnectionClosed: - raise - except KeyError as e: - raise e - - async def _start_receiving(self): - try: - while True: - await self._recv() - except asyncio.CancelledError: - pass - except ConnectionClosed: - # TODO try reconnect, but only if it's needed - raise - - async def send(self, payload: dict) -> int: - """ - Sends a payload to the websocket connection. - - :param payload: payload, generate a payload with the AsyncSubstrateInterface.make_payload method - """ - async with self._lock: - original_id = self.id - self.id += 1 - self._open_subscriptions += 1 - try: - await self.ws.send(json.dumps({**payload, **{"id": original_id}})) - return original_id - except ConnectionClosed: - raise - - async def retrieve(self, item_id: int) -> Optional[dict]: - """ - Retrieves a single item from received responses dict queue - - :param item_id: id of the item to retrieve - - :return: retrieved item - """ - while True: - async with self._lock: - if item_id in self._received: - return self._received.pop(item_id) - await asyncio.sleep(0.1) - - -class AsyncSubstrateInterface: - runtime = None - registry: Optional[PortableRegistry] = None - metadata_v15: Optional[MetadataV15] = None - - def __init__( - self, - chain_endpoint: str, - use_remote_preset=False, - auto_discover=True, - auto_reconnect=True, - ss58_format=None, - type_registry=None, - chain_name=None, - ): - """ - The asyncio-compatible version of the subtensor interface commands we use in bittensor - """ - self.chain_endpoint = chain_endpoint - self.__chain = chain_name - self.ws = Websocket( - chain_endpoint, - options={ - "max_size": 2**32, - "write_limit": 2**16, - }, - ) - self._lock = asyncio.Lock() - self.last_block_hash: Optional[str] = None - self.config = { - "use_remote_preset": use_remote_preset, - "auto_discover": auto_discover, - "auto_reconnect": auto_reconnect, - "rpc_methods": None, - "strict_scale_decode": True, - } - self.initialized = False - self._forgettable_task = None - self.ss58_format = ss58_format - self.type_registry = type_registry - self.runtime_cache = RuntimeCache() - self.block_id: Optional[int] = None - self.runtime_version = None - self.runtime_config = RuntimeConfigurationObject() - self.__metadata_cache = {} - self.type_registry_preset = None - self.transaction_version = None - self.metadata = None - self.metadata_version_hex = "0x0f000000" # v15 - self.metadata_v15 = None - - async def __aenter__(self): - await self.initialize() - - async def initialize(self): - """ - Initialize the connection to the chain. - """ - async with self._lock: - if not self.initialized: - if not self.__chain: - chain = await self.rpc_request("system_chain", []) - self.__chain = chain.get("result") - self.reload_type_registry() - await asyncio.gather(self.load_registry(), self.init_runtime(None)) - self.initialized = True - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - @property - def chain(self): - """ - Returns the substrate chain currently associated with object - """ - return self.__chain - - async def get_storage_item(self, module: str, storage_function: str): - if not self.metadata: - await self.init_runtime() - metadata_pallet = self.metadata.get_metadata_pallet(module) - storage_item = metadata_pallet.get_storage_function(storage_function) - return storage_item - - async def _get_current_block_hash( - self, block_hash: Optional[str], reuse: bool - ) -> Optional[str]: - if block_hash: - self.last_block_hash = block_hash - return block_hash - elif reuse: - if self.last_block_hash: - return self.last_block_hash - return block_hash - - async def load_registry(self): - metadata_rpc_result = await self.rpc_request( - "state_call", - ["Metadata_metadata_at_version", self.metadata_version_hex], - ) - metadata_option_hex_str = metadata_rpc_result["result"] - metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:]) - metadata_v15 = MetadataV15.decode_from_metadata_option(metadata_option_bytes) - self.registry = PortableRegistry.from_metadata_v15(metadata_v15) - self.metadata_v15 = metadata_v15 - - async def decode_scale( - self, type_string, scale_bytes: bytes, return_scale_obj=False - ): - """ - Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string - (e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash - is set - - Parameters - ---------- - type_string - scale_bytes - block_hash - return_scale_obj: if True the SCALE object itself is returned, otherwise the serialized dict value of the object - - Returns - ------- - - """ - if scale_bytes == b"\x00": - obj = None - if type_string == "scale_info::0": # Is an AccountId - # Decode AccountId bytes to SS58 address - return decode_account_id(scale_bytes) - else: - obj = decode_by_type_string(type_string, self.registry, scale_bytes) - return obj - - async def encode_scale(self, type_string, value: Any) -> bytes: - """ - Helper function to encode arbitrary objects according to given RUST type_string - (e.g. BlockNumber). - - Parameters - ---------- - type_string - value - - Returns - ------- - bytes: encoded SCALE bytes - - """ - if value is None: - result = b"\x00" - else: - if type_string == "scale_info::0": # Is an AccountId - # encode string into AccountId - ## AccountId is a composite type with one, unnamed field - return encode_account_id(value) - - elif type_string == "scale_info::151": # Vec - if not isinstance(value, (list, tuple)): - value = [value] - - # Encode length - length = len(value) - if length < 64: - result = bytes([length << 2]) # Single byte mode - else: - raise ValueError("Vector length too large") - - # Encode each AccountId - for account in value: - if isinstance(account, bytes): - result += account # Already encoded - else: - result += encode_account_id(account) # SS58 string - return result - - if isinstance(value, ScaleType): - if value.data.data is not None: - # Already encoded - return bytes(value.data.data) - else: - value = value.value # Unwrap the value of the type - - result = bytes(encode_by_type_string(type_string, self.registry, value)) - return result - - async def init_runtime( - self, block_hash: Optional[str] = None, block_id: Optional[int] = None - ) -> Runtime: - """ - This method is used by all other methods that deals with metadata and types defined in the type registry. - It optionally retrieves the block_hash when block_id is given and sets the applicable metadata for that - block_hash. Also, it applies all the versioned types at the time of the block_hash. - - Because parsing of metadata and type registry is quite heavy, the result will be cached per runtime id. - In the future there could be support for caching backends like Redis to make this cache more persistent. - - :param block_hash: optional block hash, should not be specified if block_id is - :param block_id: optional block id, should not be specified if block_hash is - - :returns: Runtime object - """ - - async def get_runtime(block_hash, block_id) -> Runtime: - # Check if runtime state already set to current block - if ( - (block_hash and block_hash == self.last_block_hash) - or (block_id and block_id == self.block_id) - ) and self.metadata is not None: - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - if block_id is not None: - block_hash = await self.get_block_hash(block_id) - - if not block_hash: - block_hash = await self.get_chain_head() - - self.last_block_hash = block_hash - self.block_id = block_id - - # In fact calls and storage functions are decoded against runtime of previous block, therefor retrieve - # metadata and apply type registry of runtime of parent block - block_header = await self.rpc_request( - "chain_getHeader", [self.last_block_hash] - ) - - if block_header["result"] is None: - raise SubstrateRequestException( - f'Block not found for "{self.last_block_hash}"' - ) - - parent_block_hash: str = block_header["result"]["parentHash"] - - if ( - parent_block_hash - == "0x0000000000000000000000000000000000000000000000000000000000000000" - ): - runtime_block_hash = self.last_block_hash - else: - runtime_block_hash = parent_block_hash - - runtime_info = await self.get_block_runtime_version( - block_hash=runtime_block_hash - ) - - if runtime_info is None: - raise SubstrateRequestException( - f"No runtime information for block '{block_hash}'" - ) - # Check if runtime state already set to current block - if ( - runtime_info.get("specVersion") == self.runtime_version - and self.metadata is not None - ): - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - self.runtime_version = runtime_info.get("specVersion") - self.transaction_version = runtime_info.get("transactionVersion") - - if not self.metadata: - if self.runtime_version in self.__metadata_cache: - # Get metadata from cache - # self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - metadata = self.metadata = self.__metadata_cache[ - self.runtime_version - ] - else: - metadata = self.metadata = await self.get_block_metadata( - block_hash=runtime_block_hash, decode=True - ) - # self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) - - # Update metadata cache - self.__metadata_cache[self.runtime_version] = self.metadata - else: - metadata = self.metadata - # Update type registry - self.reload_type_registry(use_remote_preset=False, auto_discover=True) - - if self.implements_scaleinfo: - # self.debug_message('Add PortableRegistry from metadata to type registry') - self.runtime_config.add_portable_registry(self.metadata) - - # Set active runtime version - self.runtime_config.set_active_spec_version_id(self.runtime_version) - - # Check and apply runtime constants - ss58_prefix_constant = await self.get_constant( - "System", "SS58Prefix", block_hash=block_hash - ) - - if ss58_prefix_constant: - self.ss58_format = ss58_prefix_constant - - # Set runtime compatibility flags - try: - _ = self.runtime_config.create_scale_object( - "sp_weights::weight_v2::Weight" - ) - self.config["is_weight_v2"] = True - self.runtime_config.update_type_registry_types( - {"Weight": "sp_weights::weight_v2::Weight"} - ) - except NotImplementedError: - self.config["is_weight_v2"] = False - self.runtime_config.update_type_registry_types({"Weight": "WeightV1"}) - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - if block_id and block_hash: - raise ValueError("Cannot provide block_hash and block_id at the same time") - - if ( - not (runtime := self.runtime_cache.retrieve(block_id, block_hash)) - or runtime.metadata is None - ): - runtime = await get_runtime(block_hash, block_id) - self.runtime_cache.add_item(block_id, block_hash, runtime) - return runtime - - def reload_type_registry( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - """ - Reload type registry and preset used to instantiate the SubtrateInterface object. Useful to periodically apply - changes in type definitions when a runtime upgrade occurred - - Parameters - ---------- - use_remote_preset: When True preset is downloaded from Github master, otherwise use files from local installed scalecodec package - auto_discover - - Returns - ------- - - """ - self.runtime_config.clear_type_registry() - - self.runtime_config.implements_scale_info = self.implements_scaleinfo - - # Load metadata types in runtime configuration - self.runtime_config.update_type_registry(load_type_registry_preset(name="core")) - self.apply_type_registry_presets( - use_remote_preset=use_remote_preset, auto_discover=auto_discover - ) - - def apply_type_registry_presets( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - if self.type_registry_preset is not None: - # Load type registry according to preset - type_registry_preset_dict = load_type_registry_preset( - name=self.type_registry_preset, use_remote_preset=use_remote_preset - ) - - if not type_registry_preset_dict: - raise ValueError( - f"Type registry preset '{self.type_registry_preset}' not found" - ) - - elif auto_discover: - # Try to auto discover type registry preset by chain name - type_registry_name = self.chain.lower().replace(" ", "-") - try: - type_registry_preset_dict = load_type_registry_preset( - type_registry_name - ) - # self.debug_message(f"Auto set type_registry_preset to {type_registry_name} ...") - self.type_registry_preset = type_registry_name - except ValueError: - type_registry_preset_dict = None - - else: - type_registry_preset_dict = None - - if type_registry_preset_dict: - # Load type registries in runtime configuration - if self.implements_scaleinfo is False: - # Only runtime with no embedded types in metadata need the default set of explicit defined types - self.runtime_config.update_type_registry( - load_type_registry_preset( - "legacy", use_remote_preset=use_remote_preset - ) - ) - - if self.type_registry_preset != "legacy": - self.runtime_config.update_type_registry(type_registry_preset_dict) - - if self.type_registry: - # Load type registries in runtime configuration - self.runtime_config.update_type_registry(self.type_registry) - - @property - def implements_scaleinfo(self) -> Optional[bool]: - """ - Returns True if current runtime implementation a `PortableRegistry` (`MetadataV14` and higher) - - Returns - ------- - bool - """ - if self.metadata: - return self.metadata.portable_registry is not None - else: - return None - - async def create_storage_key( - self, - pallet: str, - storage_function: str, - params: Optional[list] = None, - block_hash: str = None, - ) -> StorageKey: - """ - Create a `StorageKey` instance providing storage function details. See `subscribe_storage()`. - - Parameters - ---------- - pallet: name of pallet - storage_function: name of storage function - params: Optional list of parameters in case of a Mapped storage function - - Returns - ------- - StorageKey - """ - await self.init_runtime(block_hash=block_hash) - - return StorageKey.create_from_storage_function( - pallet, - storage_function, - params, - runtime_config=self.runtime_config, - metadata=self.metadata, - ) - - async def _get_block_handler( - self, - block_hash: str, - ignore_decoding_errors: bool = False, - include_author: bool = False, - header_only: bool = False, - finalized_only: bool = False, - subscription_handler: Optional[Callable] = None, - ): - try: - await self.init_runtime(block_hash=block_hash) - except BlockNotFound: - return None - - async def decode_block(block_data, block_data_hash=None): - if block_data: - if block_data_hash: - block_data["header"]["hash"] = block_data_hash - - if type(block_data["header"]["number"]) is str: - # Convert block number from hex (backwards compatibility) - block_data["header"]["number"] = int( - block_data["header"]["number"], 16 - ) - - extrinsic_cls = self.runtime_config.get_decoder_class("Extrinsic") - - if "extrinsics" in block_data: - for idx, extrinsic_data in enumerate(block_data["extrinsics"]): - extrinsic_decoder = extrinsic_cls( - data=ScaleBytes(extrinsic_data), - metadata=self.metadata, - runtime_config=self.runtime_config, - ) - try: - extrinsic_decoder.decode(check_remaining=True) - block_data["extrinsics"][idx] = extrinsic_decoder - - except Exception as e: - if not ignore_decoding_errors: - raise - block_data["extrinsics"][idx] = None - - for idx, log_data in enumerate(block_data["header"]["digest"]["logs"]): - if type(log_data) is str: - # Convert digest log from hex (backwards compatibility) - try: - log_digest_cls = self.runtime_config.get_decoder_class( - "sp_runtime::generic::digest::DigestItem" - ) - - if log_digest_cls is None: - raise NotImplementedError( - "No decoding class found for 'DigestItem'" - ) - - log_digest = log_digest_cls(data=ScaleBytes(log_data)) - log_digest.decode( - check_remaining=self.config.get("strict_scale_decode") - ) - - block_data["header"]["digest"]["logs"][idx] = log_digest - - if include_author and "PreRuntime" in log_digest.value: - if self.implements_scaleinfo: - engine = bytes(log_digest[1][0]) - # Retrieve validator set - parent_hash = block_data["header"]["parentHash"] - validator_set = await self.query( - "Session", "Validators", block_hash=parent_hash - ) - - if engine == b"BABE": - babe_predigest = ( - self.runtime_config.create_scale_object( - type_string="RawBabePreDigest", - data=ScaleBytes( - bytes(log_digest[1][1]) - ), - ) - ) - - babe_predigest.decode( - check_remaining=self.config.get( - "strict_scale_decode" - ) - ) - - rank_validator = babe_predigest[1].value[ - "authority_index" - ] - - block_author = validator_set[rank_validator] - block_data["author"] = block_author.value - - elif engine == b"aura": - aura_predigest = ( - self.runtime_config.create_scale_object( - type_string="RawAuraPreDigest", - data=ScaleBytes( - bytes(log_digest[1][1]) - ), - ) - ) - - aura_predigest.decode(check_remaining=True) - - rank_validator = aura_predigest.value[ - "slot_number" - ] % len(validator_set) - - block_author = validator_set[rank_validator] - block_data["author"] = block_author.value - else: - raise NotImplementedError( - f"Cannot extract author for engine {log_digest.value['PreRuntime'][0]}" - ) - else: - if ( - log_digest.value["PreRuntime"]["engine"] - == "BABE" - ): - validator_set = await self.query( - "Session", - "Validators", - block_hash=block_hash, - ) - rank_validator = log_digest.value["PreRuntime"][ - "data" - ]["authority_index"] - - block_author = validator_set.elements[ - rank_validator - ] - block_data["author"] = block_author.value - else: - raise NotImplementedError( - f"Cannot extract author for engine {log_digest.value['PreRuntime']['engine']}" - ) - - except Exception: - if not ignore_decoding_errors: - raise - block_data["header"]["digest"]["logs"][idx] = None - - return block_data - - if callable(subscription_handler): - rpc_method_prefix = "Finalized" if finalized_only else "New" - - async def result_handler(message, update_nr, subscription_id): - new_block = await decode_block({"header": message["params"]["result"]}) - - subscription_result = subscription_handler( - new_block, update_nr, subscription_id - ) - - if subscription_result is not None: - # Handler returned end result: unsubscribe from further updates - self._forgettable_task = asyncio.create_task( - self.rpc_request( - f"chain_unsubscribe{rpc_method_prefix}Heads", - [subscription_id], - ) - ) - - return subscription_result - - result = await self._make_rpc_request( - [ - self.make_payload( - "_get_block_handler", - f"chain_subscribe{rpc_method_prefix}Heads", - [], - ) - ], - result_handler=result_handler, - ) - - return result - - else: - if header_only: - response = await self.rpc_request("chain_getHeader", [block_hash]) - return await decode_block( - {"header": response["result"]}, block_data_hash=block_hash - ) - - else: - response = await self.rpc_request("chain_getBlock", [block_hash]) - return await decode_block( - response["result"]["block"], block_data_hash=block_hash - ) - - async def get_block( - self, - block_hash: Optional[str] = None, - block_number: Optional[int] = None, - ignore_decoding_errors: bool = False, - include_author: bool = False, - finalized_only: bool = False, - ) -> Optional[dict]: - """ - Retrieves a block and decodes its containing extrinsics and log digest items. If `block_hash` and `block_number` - is omitted the chain tip will be retrieve, or the finalized head if `finalized_only` is set to true. - - Either `block_hash` or `block_number` should be set, or both omitted. - - Parameters - ---------- - block_hash: the hash of the block to be retrieved - block_number: the block number to retrieved - ignore_decoding_errors: When set this will catch all decoding errors, set the item to None and continue decoding - include_author: This will retrieve the block author from the validator set and add to the result - finalized_only: when no `block_hash` or `block_number` is set, this will retrieve the finalized head - - Returns - ------- - A dict containing the extrinsic and digest logs data - """ - if block_hash and block_number: - raise ValueError("Either block_hash or block_number should be be set") - - if block_number is not None: - block_hash = await self.get_block_hash(block_number) - - if block_hash is None: - return - - if block_hash and finalized_only: - raise ValueError( - "finalized_only cannot be True when block_hash is provided" - ) - - if block_hash is None: - # Retrieve block hash - if finalized_only: - block_hash = await self.get_chain_finalised_head() - else: - block_hash = await self.get_chain_head() - - return await self._get_block_handler( - block_hash=block_hash, - ignore_decoding_errors=ignore_decoding_errors, - header_only=False, - include_author=include_author, - ) - - async def get_events(self, block_hash: Optional[str] = None) -> list: - """ - Convenience method to get events for a certain block (storage call for module 'System' and function 'Events') - - Parameters - ---------- - block_hash - - Returns - ------- - list - """ - - def convert_event_data(data): - # Extract phase information - phase_key, phase_value = next(iter(data["phase"].items())) - try: - extrinsic_idx = phase_value[0] - except IndexError: - extrinsic_idx = None - - # Extract event details - module_id, event_data = next(iter(data["event"].items())) - event_id, attributes_data = next(iter(event_data[0].items())) - - # Convert class and pays_fee dictionaries to their string equivalents if they exist - attributes = attributes_data - if isinstance(attributes, dict): - for key, value in attributes.items(): - if isinstance(value, dict): - # Convert nested single-key dictionaries to their keys as strings - sub_key = next(iter(value.keys())) - if value[sub_key] == (): - attributes[key] = sub_key - - # Create the converted dictionary - converted = { - "phase": phase_key, - "extrinsic_idx": extrinsic_idx, - "event": { - "module_id": module_id, - "event_id": event_id, - "attributes": attributes, - }, - "topics": list(data["topics"]), # Convert topics tuple to a list - } - - return converted - - events = [] - - if not block_hash: - block_hash = await self.get_chain_head() - - storage_obj = await self.query( - module="System", storage_function="Events", block_hash=block_hash - ) - if storage_obj: - for item in list(storage_obj): - # print("item!", item) - events.append(convert_event_data(item)) - # events += list(storage_obj) - return events - - async def get_block_runtime_version(self, block_hash: str) -> dict: - """ - Retrieve the runtime version id of given block_hash - """ - response = await self.rpc_request("state_getRuntimeVersion", [block_hash]) - return response.get("result") - - async def get_block_metadata( - self, block_hash: Optional[str] = None, decode: bool = True - ) -> Union[dict, ScaleType]: - """ - A pass-though to existing JSONRPC method `state_getMetadata`. - - Parameters - ---------- - block_hash - decode: True for decoded version - - Returns - ------- - - """ - params = None - if decode and not self.runtime_config: - raise ValueError( - "Cannot decode runtime configuration without a supplied runtime_config" - ) - - if block_hash: - params = [block_hash] - response = await self.rpc_request("state_getMetadata", params) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - if response.get("result") and decode: - metadata_decoder = self.runtime_config.create_scale_object( - "MetadataVersioned", data=ScaleBytes(response.get("result")) - ) - metadata_decoder.decode() - - return metadata_decoder - - return response - - async def _preprocess( - self, - query_for: Optional[list], - block_hash: Optional[str], - storage_function: str, - module: str, - ) -> Preprocessed: - """ - Creates a Preprocessed data object for passing to `_make_rpc_request` - """ - params = query_for if query_for else [] - # Search storage call in metadata - metadata_pallet = self.metadata.get_metadata_pallet(module) - - if not metadata_pallet: - raise SubstrateRequestException(f'Pallet "{module}" not found') - - storage_item = metadata_pallet.get_storage_function(storage_function) - - if not metadata_pallet or not storage_item: - raise SubstrateRequestException( - f'Storage function "{module}.{storage_function}" not found' - ) - - # SCALE type string of value - param_types = storage_item.get_params_type_string() - value_scale_type = storage_item.get_value_type_string() - - if len(params) != len(param_types): - raise ValueError( - f"Storage function requires {len(param_types)} parameters, {len(params)} given" - ) - - storage_key = StorageKey.create_from_storage_function( - module, - storage_item.value["name"], - params, - runtime_config=self.runtime_config, - metadata=self.metadata, - ) - method = "state_getStorageAt" - return Preprocessed( - str(query_for), - method, - [storage_key.to_hex(), block_hash], - value_scale_type, - storage_item, - ) - - async def _process_response( - self, - response: dict, - subscription_id: Union[int, str], - value_scale_type: Optional[str] = None, - storage_item: Optional[ScaleType] = None, - runtime: Optional[Runtime] = None, - result_handler: Optional[ResultHandler] = None, - ) -> tuple[Union[ScaleType, dict], bool]: - """ - Processes the RPC call response by decoding it, returning it as is, or setting a handler for subscriptions, - depending on the specific call. - - :param response: the RPC call response - :param subscription_id: the subscription id for subscriptions, used only for subscriptions with a result handler - :param value_scale_type: Scale Type string used for decoding ScaleBytes results - :param storage_item: The ScaleType object used for decoding ScaleBytes results - :param runtime: the runtime object, used for decoding ScaleBytes results - :param result_handler: the result handler coroutine used for handling longer-running subscriptions - - :return: (decoded response, completion) - """ - result: Union[dict, ScaleType] = response - if value_scale_type and isinstance(storage_item, ScaleType): - if not runtime: - async with self._lock: - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - if response.get("result") is not None: - query_value = response.get("result") - elif storage_item.value["modifier"] == "Default": - # Fallback to default value of storage function if no result - query_value = storage_item.value_object["default"].value_object - else: - # No result is interpreted as an Option<...> result - value_scale_type = f"Option<{value_scale_type}>" - query_value = storage_item.value_object["default"].value_object - if isinstance(query_value, str): - q = bytes.fromhex(query_value[2:]) - elif isinstance(query_value, bytearray): - q = bytes(query_value) - else: - q = query_value - obj = await self.decode_scale(value_scale_type, q, True) - result = obj - if asyncio.iscoroutinefunction(result_handler): - # For multipart responses as a result of subscriptions. - message, bool_result = await result_handler(response, subscription_id) - return message, bool_result - return result, True - - async def _make_rpc_request( - self, - payloads: list[dict], - value_scale_type: Optional[str] = None, - storage_item: Optional[ScaleType] = None, - runtime: Optional[Runtime] = None, - result_handler: Optional[ResultHandler] = None, - ) -> RequestManager.RequestResults: - request_manager = RequestManager(payloads) - - subscription_added = False - - async with self.ws as ws: - for item in payloads: - item_id = await ws.send(item["payload"]) - request_manager.add_request(item_id, item["id"]) - - while True: - for item_id in request_manager.response_map.keys(): - if ( - item_id not in request_manager.responses - or asyncio.iscoroutinefunction(result_handler) - ): - if response := await ws.retrieve(item_id): - if ( - asyncio.iscoroutinefunction(result_handler) - and not subscription_added - ): - # handles subscriptions, overwrites the previous mapping of {item_id : payload_id} - # with {subscription_id : payload_id} - try: - item_id = request_manager.overwrite_request( - item_id, response["result"] - ) - except KeyError: - raise SubstrateRequestException(str(response)) - decoded_response, complete = await self._process_response( - response, - item_id, - value_scale_type, - storage_item, - runtime, - result_handler, - ) - request_manager.add_response( - item_id, decoded_response, complete - ) - if ( - asyncio.iscoroutinefunction(result_handler) - and not subscription_added - ): - subscription_added = True - break - - if request_manager.is_complete: - break - - return request_manager.get_results() - - @staticmethod - def make_payload(id_: str, method: str, params: list) -> dict: - """ - Creates a payload for making an rpc_request with _make_rpc_request - - :param id_: a unique name you would like to give to this request - :param method: the method in the RPC request - :param params: the params in the RPC request - - :return: the payload dict - """ - return { - "id": id_, - "payload": {"jsonrpc": "2.0", "method": method, "params": params}, - } - - async def rpc_request( - self, - method: str, - params: Optional[list], - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> Any: - """ - Makes an RPC request to the subtensor. Use this only if ``self.query`` and ``self.query_multiple`` and - ``self.query_map`` do not meet your needs. - - :param method: str the method in the RPC request - :param params: list of the params in the RPC request - :param block_hash: optional str, the hash of the block — only supply this if not supplying the block - hash in the params, and not reusing the block hash - :param reuse_block_hash: optional bool, whether to reuse the block hash in the params — only mark as True - if not supplying the block hash in the params, or via the `block_hash` parameter - - :return: the response from the RPC request - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - params = params or [] - payload_id = f"{method}{random.randint(0, 7000)}" - payloads = [ - self.make_payload( - payload_id, - method, - params + [block_hash] if block_hash else params, - ) - ] - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - result = await self._make_rpc_request(payloads, runtime=runtime) - if "error" in result[payload_id][0]: - raise SubstrateRequestException(result[payload_id][0]["error"]["message"]) - if "result" in result[payload_id][0]: - return result[payload_id][0] - else: - raise SubstrateRequestException(result[payload_id][0]) - - async def get_block_hash(self, block_id: int) -> str: - return (await self.rpc_request("chain_getBlockHash", [block_id]))["result"] - - async def get_chain_head(self) -> str: - result = await self._make_rpc_request( - [ - self.make_payload( - "rpc_request", - "chain_getHead", - [], - ) - ], - runtime=Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ), - ) - self.last_block_hash = result["rpc_request"][0]["result"] - return result["rpc_request"][0]["result"] - - async def compose_call( - self, - call_module: str, - call_function: str, - call_params: Optional[dict] = None, - block_hash: Optional[str] = None, - ) -> GenericCall: - """ - Composes a call payload which can be used in an extrinsic. - - :param call_module: Name of the runtime module e.g. Balances - :param call_function: Name of the call function e.g. transfer - :param call_params: This is a dict containing the params of the call. e.g. - `{'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk', 'value': 1000000000000}` - :param block_hash: Use metadata at given block_hash to compose call - - :return: A composed call - """ - if call_params is None: - call_params = {} - - await self.init_runtime(block_hash=block_hash) - - call = self.runtime_config.create_scale_object( - type_string="Call", metadata=self.metadata - ) - - call.encode( - { - "call_module": call_module, - "call_function": call_function, - "call_args": call_params, - } - ) - - return call - - async def query_multiple( - self, - params: list, - storage_function: str, - module: str, - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> dict[str, ScaleType]: - """ - Queries the subtensor. Only use this when making multiple queries, else use ``self.query`` - """ - # By allowing for specifying the block hash, users, if they have multiple query types they want - # to do, can simply query the block hash first, and then pass multiple query_subtensor calls - # into an asyncio.gather, with the specified block hash - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - preprocessed: tuple[Preprocessed] = await asyncio.gather( - *[ - self._preprocess([x], block_hash, storage_function, module) - for x in params - ] - ) - all_info = [ - self.make_payload(item.queryable, item.method, item.params) - for item in preprocessed - ] - # These will always be the same throughout the preprocessed list, so we just grab the first one - value_scale_type = preprocessed[0].value_scale_type - storage_item = preprocessed[0].storage_item - - responses = await self._make_rpc_request( - all_info, value_scale_type, storage_item, runtime - ) - return { - param: responses[p.queryable][0] for (param, p) in zip(params, preprocessed) - } - - async def query_multi( - self, storage_keys: list[StorageKey], block_hash: Optional[str] = None - ) -> list: - """ - Query multiple storage keys in one request. - - Example: - - ``` - storage_keys = [ - substrate.create_storage_key( - "System", "Account", ["F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T"] - ), - substrate.create_storage_key( - "System", "Account", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] - ) - ] - - result = substrate.query_multi(storage_keys) - ``` - - Parameters - ---------- - storage_keys: list of StorageKey objects - block_hash: Optional block_hash of state snapshot - - Returns - ------- - list of `(storage_key, scale_obj)` tuples - """ - - await self.init_runtime(block_hash=block_hash) - - # Retrieve corresponding value - response = await self.rpc_request( - "state_queryStorageAt", [[s.to_hex() for s in storage_keys], block_hash] - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - result = [] - - storage_key_map = {s.to_hex(): s for s in storage_keys} - - for result_group in response["result"]: - for change_storage_key, change_data in result_group["changes"]: - # Decode result for specified storage_key - storage_key = storage_key_map[change_storage_key] - if change_data is None: - change_data = b"\x00" - obj = None - else: - change_data = bytes.fromhex(change_data[2:]) - if change_data == b"\x00": - obj = None - else: - obj = await self.decode_scale( - storage_key.value_scale_type, change_data - ) - result.append((storage_key, obj)) - - return result - - async def create_scale_object( - self, - type_string: str, - data: Optional[ScaleBytes] = None, - block_hash: Optional[str] = None, - **kwargs, - ) -> "ScaleType": - """ - Convenience method to create a SCALE object of type `type_string`, this will initialize the runtime - automatically at moment of `block_hash`, or chain tip if omitted. - - :param type_string: str Name of SCALE type to create - :param data: ScaleBytes Optional ScaleBytes to decode - :param block_hash: Optional block hash for moment of decoding, when omitted the chain tip will be used - :param kwargs: keyword args for the Scale Type constructor - - :return: The created Scale Type object - """ - runtime = await self.init_runtime(block_hash=block_hash) - if "metadata" not in kwargs: - kwargs["metadata"] = runtime.metadata - - return runtime.runtime_config.create_scale_object( - type_string, data=data, **kwargs - ) - - async def generate_signature_payload( - self, - call: GenericCall, - era=None, - nonce: int = 0, - tip: int = 0, - tip_asset_id: Optional[int] = None, - include_call_length: bool = False, - ) -> ScaleBytes: - # Retrieve genesis hash - genesis_hash = await self.get_block_hash(0) - - if not era: - era = "00" - - if era == "00": - # Immortal extrinsic - block_hash = genesis_hash - else: - # Determine mortality of extrinsic - era_obj = self.runtime_config.create_scale_object("Era") - - if isinstance(era, dict) and "current" not in era and "phase" not in era: - raise ValueError( - 'The era dict must contain either "current" or "phase" element to encode a valid era' - ) - - era_obj.encode(era) - block_hash = await self.get_block_hash( - block_id=era_obj.birth(era.get("current")) - ) - - # Create signature payload - signature_payload = self.runtime_config.create_scale_object( - "ExtrinsicPayloadValue" - ) - - # Process signed extensions in metadata - if "signed_extensions" in self.metadata[1][1]["extrinsic"]: - # Base signature payload - signature_payload.type_mapping = [["call", "CallBytes"]] - - # Add signed extensions to payload - signed_extensions = self.metadata.get_signed_extensions() - - if "CheckMortality" in signed_extensions: - signature_payload.type_mapping.append( - ["era", signed_extensions["CheckMortality"]["extrinsic"]] - ) - - if "CheckEra" in signed_extensions: - signature_payload.type_mapping.append( - ["era", signed_extensions["CheckEra"]["extrinsic"]] - ) - - if "CheckNonce" in signed_extensions: - signature_payload.type_mapping.append( - ["nonce", signed_extensions["CheckNonce"]["extrinsic"]] - ) - - if "ChargeTransactionPayment" in signed_extensions: - signature_payload.type_mapping.append( - ["tip", signed_extensions["ChargeTransactionPayment"]["extrinsic"]] - ) - - if "ChargeAssetTxPayment" in signed_extensions: - signature_payload.type_mapping.append( - ["asset_id", signed_extensions["ChargeAssetTxPayment"]["extrinsic"]] - ) - - if "CheckMetadataHash" in signed_extensions: - signature_payload.type_mapping.append( - ["mode", signed_extensions["CheckMetadataHash"]["extrinsic"]] - ) - - if "CheckSpecVersion" in signed_extensions: - signature_payload.type_mapping.append( - [ - "spec_version", - signed_extensions["CheckSpecVersion"]["additional_signed"], - ] - ) - - if "CheckTxVersion" in signed_extensions: - signature_payload.type_mapping.append( - [ - "transaction_version", - signed_extensions["CheckTxVersion"]["additional_signed"], - ] - ) - - if "CheckGenesis" in signed_extensions: - signature_payload.type_mapping.append( - [ - "genesis_hash", - signed_extensions["CheckGenesis"]["additional_signed"], - ] - ) - - if "CheckMortality" in signed_extensions: - signature_payload.type_mapping.append( - [ - "block_hash", - signed_extensions["CheckMortality"]["additional_signed"], - ] - ) - - if "CheckEra" in signed_extensions: - signature_payload.type_mapping.append( - ["block_hash", signed_extensions["CheckEra"]["additional_signed"]] - ) - - if "CheckMetadataHash" in signed_extensions: - signature_payload.type_mapping.append( - [ - "metadata_hash", - signed_extensions["CheckMetadataHash"]["additional_signed"], - ] - ) - - if include_call_length: - length_obj = self.runtime_config.create_scale_object("Bytes") - call_data = str(length_obj.encode(str(call.data))) - - else: - call_data = str(call.data) - - payload_dict = { - "call": call_data, - "era": era, - "nonce": nonce, - "tip": tip, - "spec_version": self.runtime_version, - "genesis_hash": genesis_hash, - "block_hash": block_hash, - "transaction_version": self.transaction_version, - "asset_id": {"tip": tip, "asset_id": tip_asset_id}, - "metadata_hash": None, - "mode": "Disabled", - } - - signature_payload.encode(payload_dict) - - if signature_payload.data.length > 256: - return ScaleBytes( - data=blake2b(signature_payload.data.data, digest_size=32).digest() - ) - - return signature_payload.data - - async def create_signed_extrinsic( - self, - call: GenericCall, - keypair: Keypair, - era: Optional[dict] = None, - nonce: Optional[int] = None, - tip: int = 0, - tip_asset_id: Optional[int] = None, - signature: Optional[Union[bytes, str]] = None, - ) -> "GenericExtrinsic": - """ - Creates an extrinsic signed by given account details - - :param call: GenericCall to create extrinsic for - :param keypair: Keypair used to sign the extrinsic - :param era: Specify mortality in blocks in follow format: - {'period': [amount_blocks]} If omitted the extrinsic is immortal - :param nonce: nonce to include in extrinsics, if omitted the current nonce is retrieved on-chain - :param tip: The tip for the block author to gain priority during network congestion - :param tip_asset_id: Optional asset ID with which to pay the tip - :param signature: Optionally provide signature if externally signed - - :return: The signed Extrinsic - """ - await self.init_runtime() - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - # Check if extrinsic version is supported - if self.metadata[1][1]["extrinsic"]["version"] != 4: # type: ignore - raise NotImplementedError( - f"Extrinsic version {self.metadata[1][1]['extrinsic']['version']} not supported" # type: ignore - ) - - # Retrieve nonce - if nonce is None: - nonce = await self.get_account_nonce(keypair.ss58_address) or 0 - - # Process era - if era is None: - era = "00" - else: - if isinstance(era, dict) and "current" not in era and "phase" not in era: - # Retrieve current block id - era["current"] = await self.get_block_number( - await self.get_chain_finalised_head() - ) - - if signature is not None: - if isinstance(signature, str) and signature[0:2] == "0x": - signature = bytes.fromhex(signature[2:]) - - # Check if signature is a MultiSignature and contains signature version - if len(signature) == 65: - signature_version = signature[0] - signature = signature[1:] - else: - signature_version = keypair.crypto_type - - else: - # Create signature payload - signature_payload = await self.generate_signature_payload( - call=call, era=era, nonce=nonce, tip=tip, tip_asset_id=tip_asset_id - ) - - # Set Signature version to crypto type of keypair - signature_version = keypair.crypto_type - - # Sign payload - signature = keypair.sign(signature_payload) - - # Create extrinsic - extrinsic = self.runtime_config.create_scale_object( - type_string="Extrinsic", metadata=self.metadata - ) - - value = { - "account_id": f"0x{keypair.public_key.hex()}", - "signature": f"0x{signature.hex()}", - "call_function": call.value["call_function"], - "call_module": call.value["call_module"], - "call_args": call.value["call_args"], - "nonce": nonce, - "era": era, - "tip": tip, - "asset_id": {"tip": tip, "asset_id": tip_asset_id}, - "mode": "Disabled", - } - - # Check if ExtrinsicSignature is MultiSignature, otherwise omit signature_version - signature_cls = self.runtime_config.get_decoder_class("ExtrinsicSignature") - if issubclass(signature_cls, self.runtime_config.get_decoder_class("Enum")): - value["signature_version"] = signature_version - - extrinsic.encode(value) - - return extrinsic - - async def get_chain_finalised_head(self): - """ - A pass-though to existing JSONRPC method `chain_getFinalizedHead` - - Returns - ------- - - """ - response = await self.rpc_request("chain_getFinalizedHead", []) - - if response is not None: - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - return response.get("result") - - async def runtime_call_wait_to_decode( - self, - api: str, - method: str, - params: Optional[Union[list, dict]] = None, - block_hash: Optional[str] = None, - ) -> tuple[str, bytes]: - """ - Calls a runtime API method - - :param api: Name of the runtime API e.g. 'TransactionPaymentApi' - :param method: Name of the method e.g. 'query_fee_details' - :param params: List of parameters needed to call the runtime API - :param block_hash: Hash of the block at which to make the runtime API call - - :return: Tuple of the runtime call type and the result bytes - """ - await self.init_runtime() - - if params is None: - params = {} - - try: - metadata_v15 = self.metadata_v15.value() - apis = {entry["name"]: entry for entry in metadata_v15["apis"]} - api_entry = apis[api] - methods = {entry["name"]: entry for entry in api_entry["methods"]} - runtime_call_def = methods[method] - except KeyError: - raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") - - if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]): - raise ValueError( - f"Number of parameter provided ({len(params)}) does not " - f"match definition {len(runtime_call_def['inputs'])}" - ) - - # Encode params - param_data = b"" - for idx, param in enumerate(runtime_call_def["inputs"]): - param_type_string = f'scale_info::{param["ty"]}' - if isinstance(params, list): - param_data += await self.encode_scale(param_type_string, params[idx]) - else: - if param["name"] not in params: - raise ValueError(f"Runtime Call param '{param['name']}' is missing") - - param_data += await self.encode_scale( - param_type_string, params[param["name"]] - ) - - # RPC request - result_data = await self.rpc_request( - "state_call", [f"{api}_{method}", param_data.hex(), block_hash] - ) - output_type_string = f'scale_info::{runtime_call_def["output"]}' - - return output_type_string, bytes_from_hex_string_result(result_data["result"]) - - async def runtime_call( - self, - api: str, - method: str, - params: Optional[Union[list, dict]] = None, - block_hash: Optional[str] = None, - ) -> Any: - """ - Calls a runtime API method and decodes the result. - """ - # Get the runtime call type and result bytes - runtime_call_type, result_bytes = await self.runtime_call_wait_to_decode( - api, method, params, block_hash - ) - - # Decode using the type - result = await self.decode_scale(runtime_call_type, result_bytes) - return result - - async def get_account_nonce(self, account_address: str) -> int: - """ - Returns current nonce for given account address - - :param account_address: SS58 formatted address - - :return: Nonce for given account address - """ - nonce_obj = await self.runtime_call( - "AccountNonceApi", "account_nonce", [account_address] - ) - return nonce_obj - - async def get_metadata_constant(self, module_name, constant_name, block_hash=None): - """ - Retrieves the details of a constant for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - module_name - constant_name - block_hash - - Returns - ------- - MetadataModuleConstants - """ - - await self.init_runtime(block_hash=block_hash) - - for module in self.metadata.pallets: - if module_name == module.name and module.constants: - for constant in module.constants: - if constant_name == constant.value["name"]: - return constant - - async def get_constant( - self, - module_name: str, - constant_name: str, - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> Optional["ScaleType"]: - """ - Returns the decoded `ScaleType` object of the constant for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - :param module_name: Name of the module to query - :param constant_name: Name of the constant to query - :param block_hash: Hash of the block at which to make the runtime API call - :param reuse_block_hash: Reuse last-used block hash if set to true - - :return: ScaleType from the runtime call - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - constant = await self.get_metadata_constant( - module_name, constant_name, block_hash=block_hash - ) - if constant: - # Decode to ScaleType - return await self.decode_scale( - constant.type, - bytes(constant.constant_value), - return_scale_obj=True, - ) - else: - return None - - async def get_payment_info( - self, call: GenericCall, keypair: Keypair - ) -> dict[str, Any]: - """ - Retrieves fee estimation via RPC for given extrinsic - - Parameters - ---------- - call: Call object to estimate fees for - keypair: Keypair of the sender, does not have to include private key because no valid signature is required - - Returns - ------- - Dict with payment info - - E.g. `{'class': 'normal', 'partialFee': 151000000, 'weight': {'ref_time': 143322000}}` - - """ - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - if not isinstance(keypair, Keypair): - raise TypeError("'keypair' must be of type Keypair") - - # No valid signature is required for fee estimation - signature = "0x" + "00" * 64 - - # Create extrinsic - extrinsic = await self.create_signed_extrinsic( - call=call, keypair=keypair, signature=signature - ) - extrinsic_len = len(extrinsic.data) - result = ( - await self.runtime_call( # Needs the call hex, not the extrinsic object - "TransactionPaymentApi", - "query_info", - [extrinsic, extrinsic_len], - ) - ) - - return result - - async def query( - self, - module: str, - storage_function: str, - params: Optional[list] = None, - block_hash: Optional[str] = None, - raw_storage_key: Optional[bytes] = None, - subscription_handler=None, - reuse_block_hash: bool = False, - ) -> "ScaleType": - """ - Queries subtensor. This should only be used when making a single request. For multiple requests, - you should use ``self.query_multiple`` - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - preprocessed: Preprocessed = await self._preprocess( - params, block_hash, storage_function, module - ) - payload = [ - self.make_payload( - preprocessed.queryable, preprocessed.method, preprocessed.params - ) - ] - value_scale_type = preprocessed.value_scale_type - storage_item = preprocessed.storage_item - - responses = await self._make_rpc_request( - payload, - value_scale_type, - storage_item, - runtime, - result_handler=subscription_handler, - ) - return responses[preprocessed.queryable][0] - - async def query_map( - self, - module: str, - storage_function: str, - params: Optional[list] = None, - block_hash: Optional[str] = None, - max_results: Optional[int] = None, - start_key: Optional[str] = None, - page_size: int = 100, - ignore_decoding_errors: bool = False, - reuse_block_hash: bool = False, - ) -> "QueryMapResult": - """ - Iterates over all key-pairs located at the given module and storage_function. The storage - item must be a map. - - Example: - - ``` - result = await substrate.query_map('System', 'Account', max_results=100) - - async for account, account_info in result: - print(f"Free balance of account '{account.value}': {account_info.value['data']['free']}") - ``` - - Note: it is important that you do not use `for x in result.records`, as this will sidestep possible - pagination. You must do `async for x in result`. - - :param module: The module name in the metadata, e.g. System or Balances. - :param storage_function: The storage function name, e.g. Account or Locks. - :param params: The input parameters in case of for example a `DoubleMap` storage function - :param block_hash: Optional block hash for result at given block, when left to None the chain tip will be used. - :param max_results: the maximum of results required, if set the query will stop fetching results when number is - reached - :param start_key: The storage key used as offset for the results, for pagination purposes - :param page_size: The results are fetched from the node RPC in chunks of this size - :param ignore_decoding_errors: When set this will catch all decoding errors, set the item to None and continue - decoding - :param reuse_block_hash: use True if you wish to make the query using the last-used block hash. Do not mark True - if supplying a block_hash - - :return: QueryMapResult object - """ - params = params or [] - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - - metadata_pallet = runtime.metadata.get_metadata_pallet(module) - if not metadata_pallet: - raise ValueError(f'Pallet "{module}" not found') - storage_item = metadata_pallet.get_storage_function(storage_function) - - if not metadata_pallet or not storage_item: - raise ValueError( - f'Storage function "{module}.{storage_function}" not found' - ) - - value_type = storage_item.get_value_type_string() - param_types = storage_item.get_params_type_string() - key_hashers = storage_item.get_param_hashers() - - # Check MapType conditions - if len(param_types) == 0: - raise ValueError("Given storage function is not a map") - if len(params) > len(param_types) - 1: - raise ValueError( - f"Storage function map can accept max {len(param_types) - 1} parameters, {len(params)} given" - ) - - # Generate storage key prefix - storage_key = StorageKey.create_from_storage_function( - module, - storage_item.value["name"], - params, - runtime_config=runtime.runtime_config, - metadata=runtime.metadata, - ) - prefix = storage_key.to_hex() - - if not start_key: - start_key = prefix - - # Make sure if the max result is smaller than the page size, adjust the page size - if max_results is not None and max_results < page_size: - page_size = max_results - - # Retrieve storage keys - response = await self.rpc_request( - method="state_getKeysPaged", - params=[prefix, page_size, start_key, block_hash], - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - result_keys = response.get("result") - - result = [] - last_key = None - - def concat_hash_len(key_hasher: str) -> int: - """ - Helper function to avoid if statements - """ - if key_hasher == "Blake2_128Concat": - return 16 - elif key_hasher == "Twox64Concat": - return 8 - elif key_hasher == "Identity": - return 0 - else: - raise ValueError("Unsupported hash type") - - if len(result_keys) > 0: - last_key = result_keys[-1] - - # Retrieve corresponding value - response = await self.rpc_request( - method="state_queryStorageAt", params=[result_keys, block_hash] - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - for result_group in response["result"]: - for item in result_group["changes"]: - try: - # Determine type string - key_type_string = [] - for n in range(len(params), len(param_types)): - key_type_string.append( - f"[u8; {concat_hash_len(key_hashers[n])}]" - ) - key_type_string.append(param_types[n]) - - item_key_obj = await self.decode_scale( - type_string=f"({', '.join(key_type_string)})", - scale_bytes=bytes.fromhex(item[0][len(prefix) :]), - return_scale_obj=True, - ) - - # strip key_hashers to use as item key - if len(param_types) - len(params) == 1: - item_key = item_key_obj[1] - else: - item_key = tuple( - item_key_obj[key + 1] - for key in range(len(params), len(param_types) + 1, 2) - ) - - except Exception as _: - if not ignore_decoding_errors: - raise - item_key = None - - try: - try: - item_bytes = bytes.fromhex(item[1][2:]) - except ValueError: - item_bytes = bytes.fromhex(item[1]) - - item_value = await self.decode_scale( - type_string=value_type, - scale_bytes=item_bytes, - return_scale_obj=True, - ) - except Exception as _: - if not ignore_decoding_errors: - raise - item_value = None - - result.append([item_key, item_value]) - - return QueryMapResult( - records=result, - page_size=page_size, - module=module, - storage_function=storage_function, - params=params, - block_hash=block_hash, - substrate=self, - last_key=last_key, - max_results=max_results, - ignore_decoding_errors=ignore_decoding_errors, - ) - - async def submit_extrinsic( - self, - extrinsic: GenericExtrinsic, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> "ExtrinsicReceipt": - """ - Submit an extrinsic to the connected node, with the possibility to wait until the extrinsic is included - in a block and/or the block is finalized. The receipt returned provided information about the block and - triggered events - - Parameters - ---------- - extrinsic: Extrinsic The extrinsic to be sent to the network - wait_for_inclusion: wait until extrinsic is included in a block (only works for websocket connections) - wait_for_finalization: wait until extrinsic is finalized (only works for websocket connections) - - Returns - ------- - ExtrinsicReceipt - - """ - - # Check requirements - if not isinstance(extrinsic, GenericExtrinsic): - raise TypeError("'extrinsic' must be of type Extrinsics") - - async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: - """ - Result handler function passed as an arg to _make_rpc_request as the result_handler - to handle the results of the extrinsic rpc call, which are multipart, and require - subscribing to the message - - :param message: message received from the rpc call - :param subscription_id: subscription id received from the initial rpc call for the subscription - - :returns: tuple containing the dict of the block info for the subscription, and bool for whether - the subscription is completed. - """ - # Check if extrinsic is included and finalized - if "params" in message and isinstance(message["params"]["result"], dict): - # Convert result enum to lower for backwards compatibility - message_result = { - k.lower(): v for k, v in message["params"]["result"].items() - } - - if "finalized" in message_result and wait_for_finalization: - # Created as a task because we don't actually care about the result - self._forgettable_task = asyncio.create_task( - self.rpc_request("author_unwatchExtrinsic", [subscription_id]) - ) - return { - "block_hash": message_result["finalized"], - "extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()), - "finalized": True, - }, True - elif ( - "inblock" in message_result - and wait_for_inclusion - and not wait_for_finalization - ): - # Created as a task because we don't actually care about the result - self._forgettable_task = asyncio.create_task( - self.rpc_request("author_unwatchExtrinsic", [subscription_id]) - ) - return { - "block_hash": message_result["inblock"], - "extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()), - "finalized": False, - }, True - return message, False - - if wait_for_inclusion or wait_for_finalization: - responses = ( - await self._make_rpc_request( - [ - self.make_payload( - "rpc_request", - "author_submitAndWatchExtrinsic", - [str(extrinsic.data)], - ) - ], - result_handler=result_handler, - ) - )["rpc_request"] - response = next( - (r for r in responses if "block_hash" in r and "extrinsic_hash" in r), - None, - ) - - if not response: - raise SubstrateRequestException(responses) - - # Also, this will be a multipart response, so maybe should change to everything after the first response? - # The following code implies this will be a single response after the initial subscription id. - result = ExtrinsicReceipt( - substrate=self, - extrinsic_hash=response["extrinsic_hash"], - block_hash=response["block_hash"], - finalized=response["finalized"], - ) - - else: - response = await self.rpc_request( - "author_submitExtrinsic", [str(extrinsic.data)] - ) - - if "result" not in response: - raise SubstrateRequestException(response.get("error")) - - result = ExtrinsicReceipt(substrate=self, extrinsic_hash=response["result"]) - - return result - - async def get_metadata_call_function( - self, - module_name: str, - call_function_name: str, - block_hash: Optional[str] = None, - ) -> Optional[list]: - """ - Retrieves a list of all call functions in metadata active for given block_hash (or chaintip if block_hash - is omitted) - - :param module_name: name of the module - :param call_function_name: name of the call function - :param block_hash: optional block hash - - :return: list of call functions - """ - runtime = await self.init_runtime(block_hash=block_hash) - - for pallet in runtime.metadata.pallets: - if pallet.name == module_name and pallet.calls: - for call in pallet.calls: - if call.name == call_function_name: - return call - return None - - async def get_block_number(self, block_hash: Optional[str]) -> int: - """Async version of `substrateinterface.base.get_block_number` method.""" - response = await self.rpc_request("chain_getHeader", [block_hash]) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - elif "result" in response: - if response["result"]: - return int(response["result"]["number"], 16) - - async def close(self): - """ - Closes the substrate connection, and the websocket connection. - """ - try: - await self.ws.shutdown() - except AttributeError: - pass diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 95e29181..790eaa64 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -98,7 +98,11 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - return False, "", format_error_message(await response.error_message, subtensor.substrate) + return ( + False, + "", + format_error_message(await response.error_message, subtensor.substrate), + ) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 63eae82e..c53e9da8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,6 +1,5 @@ import asyncio -from typing import Optional, Any, Union, TypedDict, Iterable - +from typing import Optional, Any, Union, TypedDict, Iterable, Coroutine import aiohttp from bittensor_wallet import Wallet @@ -10,10 +9,8 @@ from substrateinterface.exceptions import SubstrateRequestException import typer -from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - TimeoutException, -) + +from async_substrate_interface.async_substrate import AsyncSubstrateInterface from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, StakeInfo, @@ -38,7 +35,7 @@ u16_normalized_float, u64_normalized_float, ss58_to_vec_u8, - encode_account_id + encode_account_id, ) @@ -69,6 +66,17 @@ def decode_ss58_tuples(l: tuple): return [decode_account_id(l[x][0]) for x in range(len(l))] +async def get_value(coroutine: Coroutine) -> Any: + """ + Helper function for getting the result value from a substrate query, which is usually a ScaleObj, but not always + """ + result = await coroutine + if hasattr(result, "value"): + return result.value + else: + return result + + class SubtensorInterface: """ Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls. @@ -105,7 +113,7 @@ def __init__(self, network): self.network = defaults.subtensor.network self.substrate = AsyncSubstrateInterface( - chain_endpoint=self.chain_endpoint, + url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, chain_name="Bittensor", @@ -121,7 +129,7 @@ async def __aenter__(self): try: async with self.substrate: return self - except TimeoutException: + except TimeoutError: # TODO verify err_console.print( "\n[red]Error[/red]: Timeout occurred connecting to substrate. " f"Verify your chain and network settings: {self}" @@ -169,11 +177,11 @@ async def get_all_subnet_netuids( block_hash=block_hash, reuse_block_hash=True, ) - return ( - [] - if result is None or not hasattr(result, "records") - else [netuid async for netuid, exists in result if exists] - ) + res = [] + async for netuid, exists in result: + if exists.value: # TODO verify type + res.append(netuid) + return res async def is_hotkey_delegate( self, @@ -270,25 +278,31 @@ async def get_stake_for_coldkey_and_hotkey( Returns: Balance: The stake under the coldkey - hotkey pairing. """ - alpha_shares = await self.substrate.query( - module="SubtensorModule", - storage_function="Alpha", - params=[hotkey_ss58, coldkey_ss58, netuid], - block_hash=block_hash, + alpha_shares = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="Alpha", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ) ) - hotkey_alpha = await self.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyAlpha", - params=[hotkey_ss58, netuid], - block_hash=block_hash, + hotkey_alpha = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="TotalHotkeyAlpha", + params=[hotkey_ss58, netuid], + block_hash=block_hash, + ) ) - hotkey_shares = await self.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyShares", - params=[hotkey_ss58, netuid], - block_hash=block_hash, + hotkey_shares = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="TotalHotkeyShares", + params=[hotkey_ss58, netuid], + block_hash=block_hash, + ) ) alpha_shares_as_float = fixed_to_float(alpha_shares or 0) @@ -342,18 +356,20 @@ async def get_balance( ) -> Balance: """ Retrieves the balance for a single coldkey address - + :param address: coldkey address :param block_hash: the block hash, optional :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: Balance object representing the address's balance """ - result = await self.substrate.query( - module="System", - storage_function="Account", - params=[address], - block_hash=block_hash, - reuse_block_hash=reuse_block, + result = await get_value( + self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) value = result or {"data": {"free": 0}} return Balance(value["data"]["free"]) @@ -505,12 +521,14 @@ async def current_take( The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. """ - result = await self.substrate.query( - module="SubtensorModule", - storage_function="Delegates", - params=[hotkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, + result = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="Delegates", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) return u16_normalized_float(result) @@ -539,11 +557,10 @@ async def get_netuids_for_hotkey( block_hash=block_hash, reuse_block_hash=reuse_block, ) - return ( - [record[0] async for record in result if record[1]] - if result and hasattr(result, "records") - else [] - ) + res = [] + async for record in result: + if record[1].value: + res.append(record[0]) async def subnet_exists( self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False @@ -560,12 +577,14 @@ async def subnet_exists( 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 = await self.substrate.query( - module="SubtensorModule", - storage_function="NetworksAdded", - params=[netuid], - block_hash=block_hash, - reuse_block_hash=reuse_block, + result = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) return result @@ -615,12 +634,14 @@ async def get_hyperparameter( print("subnet does not exist") return None - result = await self.substrate.query( - module="SubtensorModule", - storage_function=param_name, - params=[netuid], - block_hash=block_hash, - reuse_block_hash=reuse_block, + result = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function=param_name, + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) if result is None: @@ -868,14 +889,12 @@ async def query_all_identities( block_hash=block_hash, reuse_block_hash=reuse_block, ) + all_identities = {} + async for ss58_address, identity in identities: + all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity( + identity.value + ) - if identities is None: - return {} - - all_identities = { - decode_account_id(ss58_address[0]): decode_hex_identity(identity) - for ss58_address, identity in identities - } return all_identities async def query_identity( @@ -902,12 +921,14 @@ async def query_identity( 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. """ - identity_info = await self.substrate.query( - module="SubtensorModule", - storage_function="Identities", - params=[key], - block_hash=block_hash, - reuse_block_hash=reuse_block, + identity_info = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="Identities", + params=[key], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) if not identity_info: return {} @@ -981,7 +1002,9 @@ async def weights( params=[netuid], block_hash=block_hash, ) - w_map = [(uid, w or []) async for uid, w in w_map_encoded] + w_map = [] + async for uid, w in w_map_encoded: + w_map.append((uid, w.value)) return w_map @@ -1009,7 +1032,9 @@ async def bonds( params=[netuid], block_hash=block_hash, ) - b_map = [(uid, b) async for uid, b in b_map_encoded] + b_map = [] + async for uid, b in b_map_encoded: + b_map.append((uid, b)) return b_map @@ -1028,14 +1053,16 @@ async def does_hotkey_exist( :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ - _result = await self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, + _result = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) - result = decode_account_id(_result[0]) + result = decode_account_id(_result[0]) # TODO verify return_val = ( False if result is None @@ -1046,11 +1073,13 @@ async def does_hotkey_exist( async def get_hotkey_owner( self, hotkey_ss58: str, block_hash: str ) -> Optional[str]: - hk_owner_query = await self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=block_hash, + hk_owner_query = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=block_hash, + ) ) val = decode_account_id(hk_owner_query[0]) if val: @@ -1111,10 +1140,12 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: message (if applicable) """ try: - children = await self.substrate.query( - module="SubtensorModule", - storage_function="ChildKeys", - params=[hotkey, netuid], + children = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + ) ) if children: formatted_children = [] @@ -1156,9 +1187,7 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) - async def burn_cost( - self, block_hash: Optional[str] = None - ) -> Optional[Balance]: + async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]: result = await self.query_runtime_api( runtime_api="SubnetRegistrationRuntimeApi", method="get_network_registration_cost", @@ -1186,12 +1215,14 @@ async def get_vote_data( 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 = await self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=block_hash, - reuse_block_hash=reuse_block, + vote_data = await get_value( + self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) if vote_data is None: return None @@ -1223,12 +1254,17 @@ async def get_delegate_identities( session.get(Constants.delegates_detail_url), ) - all_delegates_details = { - decode_account_id(ss58_address[0]): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity["info"]) + all_delegates_details = {} + async for ss58_address, identity in identities_info: + all_delegates_details.update( + { + decode_account_id( + ss58_address[0] + ): DelegatesDetails.from_chain_data( + decode_hex_identity_dict(identity.value["info"]) + ) + } ) - for ss58_address, identity in identities_info - } if response.ok: all_delegates: dict[str, Any] = await response.json(content_type=None) @@ -1298,8 +1334,13 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( block_hash: Optional[str] = None, ) -> "Balance": """Returns the stake under a coldkey - hotkey - netuid pairing""" - _result = await self.substrate.query( - "SubtensorModule", "Alpha", [hotkey_ss58, coldkey_ss58, netuid], block_hash + _result = await get_value( + self.substrate.query( + "SubtensorModule", + "Alpha", + [hotkey_ss58, coldkey_ss58, netuid], + block_hash, + ) ) if _result is None: return Balance(0).set_unit(netuid) @@ -1435,12 +1476,14 @@ async def get_owned_hotkeys( :return: A list of hotkey SS58 addresses owned by the coldkey. """ - owned_hotkeys = await self.substrate.query( - module="SubtensorModule", - storage_function="OwnedHotkeys", - params=[coldkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, + owned_hotkeys = await get_value( + self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) ) return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index b86822d6..3a458ca1 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -35,9 +35,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters - from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - ) + from async_substrate_interface.async_substrate import AsyncSubstrateInterface console = Console() err_console = Console(stderr=True) diff --git a/requirements.txt b/requirements.txt index 8f59d944..4de9caaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ bt-decode==v0.5.0-a0 plotille pywry plotly +git+https://github.com/opentensor/async-substrate-interface.git@staging # only for testing \ No newline at end of file diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 5dcd9afb..b10108ed 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -8,9 +8,7 @@ import time import pytest -from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, -) +from async_substrate_interface.async_substrate import AsyncSubstrateInterface from .utils import setup_wallet diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 9a339987..ebad8ff4 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -10,9 +10,7 @@ from typer.testing import CliRunner if TYPE_CHECKING: - from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - ) + from async_substrate_interface.async_substrate import AsyncSubstrateInterface template_path = os.getcwd() + "/neurons/" templates_repo = "templates repository" From 9de9edf17fce71c8f09dd2b6a65eb5d76e015c43 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 00:41:40 +0200 Subject: [PATCH 237/332] Check-in --- .../src/bittensor/extrinsics/registration.py | 31 +++++++---- .../src/bittensor/extrinsics/root.py | 20 ++++--- bittensor_cli/src/bittensor/minigraph.py | 24 ++++++--- .../src/commands/stake/children_hotkeys.py | 12 +++-- bittensor_cli/src/commands/stake/stake.py | 8 ++- bittensor_cli/src/commands/sudo.py | 54 ++++++++++++------- bittensor_cli/src/commands/wallets.py | 2 +- tests/e2e_tests/conftest.py | 2 +- 8 files changed, 103 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 721ce8f2..ff7343d0 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -438,10 +438,14 @@ async def is_hotkey_registered( subtensor: "SubtensorInterface", netuid: int, hotkey_ss58: str ) -> bool: """Checks to see if the hotkey is registered on a given netuid""" - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], + _result = getattr( + await subtensor.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + ), + "value", + None, ) if _result is not None: return True @@ -489,8 +493,12 @@ async def register_extrinsic( """ async def get_neuron_for_pubkey_and_subnet(): - uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + uid = getattr( + await subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ), + "value", + None, ) if uid is None: return NeuronInfo.get_null_neuron() @@ -707,8 +715,12 @@ async def burned_register_extrinsic( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", spinner="aesthetic", ) as status: - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + my_uid = getattr( + await subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ), + "value", + None, ) print_verbose("Checking if already registered", status) @@ -751,7 +763,7 @@ async def burned_register_extrinsic( else: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): block_hash = await subtensor.substrate.get_chain_head() - new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( + new_balance, netuids_for_hotkey, my_uid_ = await asyncio.gather( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash, @@ -764,6 +776,7 @@ async def burned_register_extrinsic( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ), ) + my_uid = getattr(my_uid_, "value", None) console.print( "Balance:\n" diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 295ee640..38aa40fd 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -342,10 +342,14 @@ async def root_register_extrinsic( # Successful registration, final check for neuron and pubkey else: - uid = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[0, wallet.hotkey.ss58_address], + uid = getattr( + await subtensor.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[0, wallet.hotkey.ss58_address], + ), + "value", + None, ) if uid is not None: console.print( @@ -419,8 +423,12 @@ async def _do_set_weights(): else: return False, await response.error_message - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] + my_uid = getattr( + await subtensor.substrate.query( + "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] + ), + "value", + None, ) if my_uid is None: diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index 3d652d6d..dee99ecb 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -215,18 +215,26 @@ async def _process_root_weights(self, data, attribute: str) -> NDArray: """ async def get_total_subnets(): - _result = await self.subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", - params=[], - reuse_block_hash=True, + _result = getattr( + await self.subtensor.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + reuse_block_hash=True, + ), + "value", + None, ) return _result async def get_subnets(): - _result = await self.subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", + _result = getattr( + await self.subtensor.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + ), + "value", + None, ) return [i for i in range(_result)] diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 52d30dd1..2b1251b0 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -227,10 +227,14 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int - Optional[float]: The value of the "ChildkeyTake" if found, or None if any error occurs. """ try: - childkey_take_ = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="ChildkeyTake", - params=[hotkey, netuid], + childkey_take_ = getattr( + await subtensor.substrate.query( + module="SubtensorModule", + storage_function="ChildkeyTake", + params=[hotkey, netuid], + ), + "value", + None, ) if childkey_take_: return int(childkey_take_.value) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 6ec589d4..76fc16c6 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -353,8 +353,12 @@ async def send_extrinsic( await extrinsics_coroutines[0] else: with console.status(":satellite: Checking transaction rate limit ..."): - tx_rate_limit_blocks = await subtensor.substrate.query( - module="SubtensorModule", storage_function="TxRateLimit" + tx_rate_limit_blocks = getattr( + await subtensor.substrate.query( + module="SubtensorModule", storage_function="TxRateLimit" + ), + "value", + None, ) netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs): diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 19fb847d..4b3c96bb 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -95,10 +95,14 @@ async def set_hyperparameter_extrinsic( finalization/inclusion, the response is `True`. """ print_verbose("Confirming subnet owner") - subnet_owner_ = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="SubnetOwner", - params=[netuid], + subnet_owner_ = getattr( + await subtensor.substrate.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ), + "value", + None, ) subnet_owner = decode_account_id(subnet_owner_[0]) if subnet_owner != wallet.coldkeypub.ss58_address: @@ -183,11 +187,15 @@ async def _get_senate_members( :return: list of the senate members' ss58 addresses """ - senate_members = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - block_hash=block_hash, + senate_members = getattr( + await subtensor.substrate.query( + module="SenateMembers", + storage_function="Members", + params=None, + block_hash=block_hash, + ), + "value", + None, ) try: return [ @@ -202,19 +210,27 @@ async def _get_proposals( subtensor: "SubtensorInterface", block_hash: str ) -> dict[str, tuple[dict, "ProposalVoteData"]]: async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: - proposal_data = await subtensor.substrate.query( - module="Triumvirate", - storage_function="ProposalOf", - block_hash=block_hash, - params=[p_hash], + proposal_data = getattr( + await subtensor.substrate.query( + module="Triumvirate", + storage_function="ProposalOf", + block_hash=block_hash, + params=[p_hash], + ), + "value", + None, ) return proposal_data - ph = await subtensor.substrate.query( - module="Triumvirate", - storage_function="Proposals", - params=None, - block_hash=block_hash, + ph = getattr( + await subtensor.substrate.query( + module="Triumvirate", + storage_function="Proposals", + params=None, + block_hash=block_hash, + ), + "value", + None, ) try: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index f0cbfc43..ad60a5ce 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1376,7 +1376,7 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): - identity = await subtensor.query_identity(ss58_address) + identity = getattr(await subtensor.query_identity(ss58_address), "value", None) if not identity: err_console.print( diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index b10108ed..4d9f9c7b 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -56,7 +56,7 @@ def wait_for_node_start(process, pattern): wait_for_node_start(process, pattern) # Run the test, passing in substrate interface - yield AsyncSubstrateInterface(chain_endpoint="ws://127.0.0.1:9945") + yield AsyncSubstrateInterface(url="ws://127.0.0.1:9945") # Terminate the process group (includes all child processes) os.killpg(os.getpgid(process.pid), signal.SIGTERM) From 9824101210c12c4a4ed2668a661e539c5681a023 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 21:04:22 +0200 Subject: [PATCH 238/332] Check-in --- bittensor_cli/__init__.py | 4 +- .../src/bittensor/extrinsics/registration.py | 33 +-- .../src/bittensor/extrinsics/root.py | 20 +- bittensor_cli/src/bittensor/minigraph.py | 24 +- .../src/bittensor/subtensor_interface.py | 225 ++++++++---------- .../src/commands/stake/children_hotkeys.py | 12 +- bittensor_cli/src/commands/stake/stake.py | 8 +- bittensor_cli/src/commands/subnets/subnets.py | 2 +- bittensor_cli/src/commands/sudo.py | 54 ++--- bittensor_cli/src/commands/wallets.py | 10 +- requirements.txt | 6 +- 11 files changed, 164 insertions(+), 234 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 8ecf1c97..3f62132b 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc14" +__version__ = "9.0.0" -__all__ = ["CLIManager", "__version__"] +__all__ = [CLIManager, __version__] diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index ff7343d0..7eb10a1a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -438,14 +438,10 @@ async def is_hotkey_registered( subtensor: "SubtensorInterface", netuid: int, hotkey_ss58: str ) -> bool: """Checks to see if the hotkey is registered on a given netuid""" - _result = getattr( - await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], - ), - "value", - None, + _result = await subtensor.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], ) if _result is not None: return True @@ -493,12 +489,8 @@ async def register_extrinsic( """ async def get_neuron_for_pubkey_and_subnet(): - uid = getattr( - await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ), - "value", - None, + uid = await subtensor.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) if uid is None: return NeuronInfo.get_null_neuron() @@ -715,12 +707,8 @@ async def burned_register_extrinsic( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", spinner="aesthetic", ) as status: - my_uid = getattr( - await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ), - "value", - None, + my_uid = await subtensor.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) print_verbose("Checking if already registered", status) @@ -763,7 +751,7 @@ async def burned_register_extrinsic( else: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): block_hash = await subtensor.substrate.get_chain_head() - new_balance, netuids_for_hotkey, my_uid_ = await asyncio.gather( + new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash, @@ -772,11 +760,10 @@ async def burned_register_extrinsic( subtensor.get_netuids_for_hotkey( wallet.hotkey.ss58_address, block_hash=block_hash ), - subtensor.substrate.query( + subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ), ) - my_uid = getattr(my_uid_, "value", None) console.print( "Balance:\n" diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 38aa40fd..d5df6eed 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -342,14 +342,10 @@ async def root_register_extrinsic( # Successful registration, final check for neuron and pubkey else: - uid = getattr( - await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[0, wallet.hotkey.ss58_address], - ), - "value", - None, + uid = await subtensor.query( + module="SubtensorModule", + storage_function="Uids", + params=[0, wallet.hotkey.ss58_address], ) if uid is not None: console.print( @@ -423,12 +419,8 @@ async def _do_set_weights(): else: return False, await response.error_message - my_uid = getattr( - await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] - ), - "value", - None, + my_uid = await subtensor.query( + "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] ) if my_uid is None: diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index dee99ecb..9e149c1b 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -215,26 +215,18 @@ async def _process_root_weights(self, data, attribute: str) -> NDArray: """ async def get_total_subnets(): - _result = getattr( - await self.subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", - params=[], - reuse_block_hash=True, - ), - "value", - None, + _result = await self.subtensor.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + reuse_block_hash=True, ) return _result async def get_subnets(): - _result = getattr( - await self.subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", - ), - "value", - None, + _result = await self.subtensor.query( + module="SubtensorModule", + storage_function="TotalNetworks", ) return [i for i in range(_result)] diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index c53e9da8..a27ade5e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -66,17 +66,6 @@ def decode_ss58_tuples(l: tuple): return [decode_account_id(l[x][0]) for x in range(len(l))] -async def get_value(coroutine: Coroutine) -> Any: - """ - Helper function for getting the result value from a substrate query, which is usually a ScaleObj, but not always - """ - result = await coroutine - if hasattr(result, "value"): - return result.value - else: - return result - - class SubtensorInterface: """ Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls. @@ -159,6 +148,30 @@ async def encode_params( return param_data.to_hex() + async def query( + self, + module: str, + storage_function: str, + params: Optional[list] = None, + block_hash: Optional[str] = None, + raw_storage_key: Optional[bytes] = None, + subscription_handler=None, + reuse_block_hash: bool = False, + ): + result = await self.substrate.query( + module, + storage_function, + params, + block_hash, + raw_storage_key, + subscription_handler, + reuse_block_hash, + ) + if hasattr(result, "value"): + return result.value + else: + return result + async def get_all_subnet_netuids( self, block_hash: Optional[str] = None ) -> list[int]: @@ -278,31 +291,25 @@ async def get_stake_for_coldkey_and_hotkey( Returns: Balance: The stake under the coldkey - hotkey pairing. """ - alpha_shares = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="Alpha", - params=[hotkey_ss58, coldkey_ss58, netuid], - block_hash=block_hash, - ) + alpha_shares = await self.query( + module="SubtensorModule", + storage_function="Alpha", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, ) - hotkey_alpha = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyAlpha", - params=[hotkey_ss58, netuid], - block_hash=block_hash, - ) + hotkey_alpha = await self.query( + module="SubtensorModule", + storage_function="TotalHotkeyAlpha", + params=[hotkey_ss58, netuid], + block_hash=block_hash, ) - hotkey_shares = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyShares", - params=[hotkey_ss58, netuid], - block_hash=block_hash, - ) + hotkey_shares = await self.query( + module="SubtensorModule", + storage_function="TotalHotkeyShares", + params=[hotkey_ss58, netuid], + block_hash=block_hash, ) alpha_shares_as_float = fixed_to_float(alpha_shares or 0) @@ -342,6 +349,8 @@ async def query_runtime_api( This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ + if reuse_block: + block_hash = self.substrate.last_block_hash result = await self.substrate.runtime_call( runtime_api, method, params, block_hash ) @@ -362,14 +371,12 @@ async def get_balance( :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: Balance object representing the address's balance """ - result = await get_value( - self.substrate.query( - module="System", - storage_function="Account", - params=[address], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + result = await self.query( + module="System", + storage_function="Account", + params=[address], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) value = result or {"data": {"free": 0}} return Balance(value["data"]["free"]) @@ -521,14 +528,12 @@ async def current_take( The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. """ - result = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="Delegates", - params=[hotkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + result = await self.query( + module="SubtensorModule", + storage_function="Delegates", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) return u16_normalized_float(result) @@ -577,14 +582,12 @@ async def subnet_exists( 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 = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="NetworksAdded", - params=[netuid], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + result = await self.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) return result @@ -634,14 +637,12 @@ async def get_hyperparameter( print("subnet does not exist") return None - result = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function=param_name, - params=[netuid], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + result = await self.query( + module="SubtensorModule", + storage_function=param_name, + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) if result is None: @@ -921,14 +922,12 @@ async def query_identity( 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. """ - identity_info = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="Identities", - params=[key], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + identity_info = await self.query( + module="SubtensorModule", + storage_function="Identities", + params=[key], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) if not identity_info: return {} @@ -1053,33 +1052,25 @@ async def does_hotkey_exist( :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ - _result = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + _result = await self.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) result = decode_account_id(_result[0]) # TODO verify - return_val = ( - False - if result is None - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) + return_val = result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" return return_val async def get_hotkey_owner( self, hotkey_ss58: str, block_hash: str ) -> Optional[str]: - hk_owner_query = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], - block_hash=block_hash, - ) + hk_owner_query = await self.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=block_hash, ) val = decode_account_id(hk_owner_query[0]) if val: @@ -1140,12 +1131,10 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: message (if applicable) """ try: - children = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="ChildKeys", - params=[hotkey, netuid], - ) + children = await self.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], ) if children: formatted_children = [] @@ -1215,14 +1204,12 @@ async def get_vote_data( 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 = await get_value( - self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + vote_data = await self.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) if vote_data is None: return None @@ -1334,13 +1321,11 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( block_hash: Optional[str] = None, ) -> "Balance": """Returns the stake under a coldkey - hotkey - netuid pairing""" - _result = await get_value( - self.substrate.query( - "SubtensorModule", - "Alpha", - [hotkey_ss58, coldkey_ss58, netuid], - block_hash, - ) + _result = await self.query( + "SubtensorModule", + "Alpha", + [hotkey_ss58, coldkey_ss58, netuid], + block_hash, ) if _result is None: return Balance(0).set_unit(netuid) @@ -1476,14 +1461,12 @@ async def get_owned_hotkeys( :return: A list of hotkey SS58 addresses owned by the coldkey. """ - owned_hotkeys = await get_value( - self.substrate.query( - module="SubtensorModule", - storage_function="OwnedHotkeys", - params=[coldkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + owned_hotkeys = await self.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 2b1251b0..053218bc 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -227,14 +227,10 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int - Optional[float]: The value of the "ChildkeyTake" if found, or None if any error occurs. """ try: - childkey_take_ = getattr( - await subtensor.substrate.query( - module="SubtensorModule", - storage_function="ChildkeyTake", - params=[hotkey, netuid], - ), - "value", - None, + childkey_take_ = await subtensor.query( + module="SubtensorModule", + storage_function="ChildkeyTake", + params=[hotkey, netuid], ) if childkey_take_: return int(childkey_take_.value) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 76fc16c6..3af7dfde 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -353,12 +353,8 @@ async def send_extrinsic( await extrinsics_coroutines[0] else: with console.status(":satellite: Checking transaction rate limit ..."): - tx_rate_limit_blocks = getattr( - await subtensor.substrate.query( - module="SubtensorModule", storage_function="TxRateLimit" - ), - "value", - None, + tx_rate_limit_blocks = await subtensor.query( + module="SubtensorModule", storage_function="TxRateLimit" ) netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs): diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 51ec34f5..6f4acae1 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1572,7 +1572,7 @@ async def metagraph_cmd( subtensor.get_hyperparameter( param_name="Difficulty", netuid=netuid, block_hash=block_hash ), - subtensor.substrate.query( + subtensor.query( module="SubtensorModule", storage_function="TotalIssuance", params=[], diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4b3c96bb..20b50605 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -95,14 +95,10 @@ async def set_hyperparameter_extrinsic( finalization/inclusion, the response is `True`. """ print_verbose("Confirming subnet owner") - subnet_owner_ = getattr( - await subtensor.substrate.query( - module="SubtensorModule", - storage_function="SubnetOwner", - params=[netuid], - ), - "value", - None, + subnet_owner_ = await subtensor.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], ) subnet_owner = decode_account_id(subnet_owner_[0]) if subnet_owner != wallet.coldkeypub.ss58_address: @@ -187,15 +183,11 @@ async def _get_senate_members( :return: list of the senate members' ss58 addresses """ - senate_members = getattr( - await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - block_hash=block_hash, - ), - "value", - None, + senate_members = await subtensor.query( + module="SenateMembers", + storage_function="Members", + params=None, + block_hash=block_hash, ) try: return [ @@ -210,27 +202,19 @@ async def _get_proposals( subtensor: "SubtensorInterface", block_hash: str ) -> dict[str, tuple[dict, "ProposalVoteData"]]: async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: - proposal_data = getattr( - await subtensor.substrate.query( - module="Triumvirate", - storage_function="ProposalOf", - block_hash=block_hash, - params=[p_hash], - ), - "value", - None, + proposal_data = await subtensor.query( + module="Triumvirate", + storage_function="ProposalOf", + block_hash=block_hash, + params=[p_hash], ) return proposal_data - ph = getattr( - await subtensor.substrate.query( - module="Triumvirate", - storage_function="Proposals", - params=None, - block_hash=block_hash, - ), - "value", - None, + ph = await subtensor.query( + module="Triumvirate", + storage_function="Proposals", + params=None, + block_hash=block_hash, ) try: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ad60a5ce..b664dcbb 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1376,7 +1376,7 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): - identity = getattr(await subtensor.query_identity(ss58_address), "value", None) + identity = await subtensor.query_identity(ss58_address) if not identity: err_console.print( @@ -1396,21 +1396,21 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): - arbitration_check = len( + arbitration_check = len( # TODO verify this works ( - await subtensor.substrate.query( + await subtensor.query( module="SubtensorModule", storage_function="ColdkeySwapDestinations", params=[wallet.coldkeypub.ss58_address], ) - ).decode() + ) ) if arbitration_check == 0: console.print( "[green]There has been no previous key swap initiated for your coldkey.[/green]" ) elif arbitration_check == 1: - arbitration_block = await subtensor.substrate.query( + arbitration_block = await subtensor.query( module="SubtensorModule", storage_function="ColdkeyArbitrationBlock", params=[wallet.coldkeypub.ss58_address], diff --git a/requirements.txt b/requirements.txt index 4de9caaf..e21d8a2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ wheel async-property==0.2.2 +async-substrate-interface==1.0.0rc7 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 @@ -16,9 +17,8 @@ scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 -bittensor-wallet>=2.0.2 +bittensor-wallet>=3.0.0 bt-decode==v0.5.0-a0 plotille pywry -plotly -git+https://github.com/opentensor/async-substrate-interface.git@staging # only for testing \ No newline at end of file +plotly \ No newline at end of file From 44d5a500108eb6f573c149bfee0270b3e8f16caa Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 21:13:01 +0200 Subject: [PATCH 239/332] Check-in --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a27ade5e..04e5dcbb 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -787,7 +787,7 @@ async def neurons_lite( method="get_neurons_lite", params=[ netuid - ], # TODO check to see if this can accept more than one at a time + ], block_hash=block_hash, reuse_block=reuse_block, ) @@ -1427,7 +1427,7 @@ async def get_stake_for_coldkeys( async def all_subnets( self, block_hash: Optional[str] = None - ) -> list["DynamicInfo"]: + ) -> list[DynamicInfo]: result = await self.substrate.runtime_call( "SubnetInfoRuntimeApi", "get_all_dynamic_info", From 1fa3c4fdec11c46e28c76142eb017ad1d04d1426 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 21:36:28 +0200 Subject: [PATCH 240/332] Runtime fix, removed unused methods. --- bittensor_cli/src/bittensor/balances.py | 13 +- .../src/bittensor/subtensor_interface.py | 117 +++++------------- 2 files changed, 29 insertions(+), 101 deletions(-) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 70656e58..f1bb90fb 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -303,18 +303,7 @@ def set_unit(self, netuid: int): return self -class FixedPoint(TypedDict): - """ - Represents a fixed point ``U64F64`` number. - Where ``bits`` is a U128 representation of the fixed point number. - - This matches the type of the Alpha shares. - """ - - bits: int - - -def fixed_to_float(fixed: FixedPoint) -> float: +def fixed_to_float(fixed: dict) -> float: # Currently this is stored as a U64F64 # which is 64 bits of integer and 64 bits of fractional uint_bits = 64 diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 04e5dcbb..f5f9dac3 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional, Any, Union, TypedDict, Iterable, Coroutine +from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp from bittensor_wallet import Wallet @@ -24,7 +24,7 @@ SubnetState, ) from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.balances import Balance, FixedPoint, fixed_to_float +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY from bittensor_cli.src.bittensor.utils import ( format_error_message, @@ -33,9 +33,6 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, - u64_normalized_float, - ss58_to_vec_u8, - encode_account_id, ) @@ -128,26 +125,6 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): await self.substrate.close() - async def encode_params( - self, - call_definition: list["ParamWithTypes"], - params: Union[list[Any], dict[str, Any]], - ) -> str: - """Returns a hex encoded string of the params using their types.""" - param_data = scalecodec.ScaleBytes(b"") - - for i, param in enumerate(call_definition["params"]): # type: ignore - scale_obj = await self.substrate.create_scale_object(param["type"]) - if isinstance(params, list): - param_data += scale_obj.encode(params[i]) - else: - if param["name"] not in params: - raise ValueError(f"Missing param {param['name']} in params dict.") - - param_data += scale_obj.encode(params[param["name"]]) - - return param_data.to_hex() - async def query( self, module: str, @@ -157,7 +134,10 @@ async def query( raw_storage_key: Optional[bytes] = None, subscription_handler=None, reuse_block_hash: bool = False, - ): + ) -> Any: + """ + Pass-through to substrate.query which automatically returns the .value if it's a ScaleObj + """ result = await self.substrate.query( module, storage_function, @@ -196,49 +176,6 @@ async def get_all_subnet_netuids( res.append(netuid) return res - async def is_hotkey_delegate( - self, - hotkey_ss58: str, - block_hash: Optional[str] = None, - reuse_block: Optional[bool] = False, - ) -> 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. - - :param hotkey_ss58: The SS58 address of the neuron's hotkey. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. - - :return: `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 = await self.get_delegates( - block_hash=block_hash, reuse_block=reuse_block - ) - return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] - - async def get_delegates( - self, block_hash: Optional[str] = None, reuse_block: Optional[bool] = False - ) -> list[DelegateInfo]: - """ - Fetches all delegates on the chain - - :param block_hash: hash of the blockchain block number for the query. - :param reuse_block: whether to reuse the last-used block hash. - - :return: List of DelegateInfo objects, or an empty list if there are no delegates. - """ - result = await self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegates", - params=[], - block_hash=block_hash, - ) - return DelegateInfo.list_from_any(result) if result is not None else [] - async def get_stake_for_coldkey( self, coldkey_ss58: str, @@ -329,7 +266,7 @@ async def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[list[int]], list[int], dict[str, int]]], + params: Optional[Union[list, dict]] = None, block_hash: Optional[str] = None, reuse_block: Optional[bool] = False, ) -> Optional[Any]: @@ -351,9 +288,9 @@ async def query_runtime_api( """ if reuse_block: block_hash = self.substrate.last_block_hash - result = await self.substrate.runtime_call( - runtime_api, method, params, block_hash - ) + result = ( + await self.substrate.runtime_call(runtime_api, method, params, block_hash) + ).value return result @@ -394,6 +331,8 @@ async def get_balances( :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: dict of {address: Balance objects} """ + if reuse_block: + block_hash = self.substrate.last_block_hash calls = [ ( await self.substrate.create_storage_key( @@ -513,17 +452,18 @@ async def current_take( hotkey_ss58: int, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> Optional[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. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. + hotkey_ss58: The `SS58` address of the neuron's hotkey. + block_hash: The hash of the block number to retrieve the stake from. + reuse_block: Whether to reuse the last-used block hash when retrieving info. Returns: - Optional[float]: The delegate take percentage, None if not available. + The delegate take percentage, None if not available. The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. @@ -535,7 +475,10 @@ async def current_take( block_hash=block_hash, reuse_block_hash=reuse_block, ) - return u16_normalized_float(result) + if result is None: + return None + else: + return u16_normalized_float(result) async def get_netuids_for_hotkey( self, @@ -566,6 +509,7 @@ async def get_netuids_for_hotkey( async for record in result: if record[1].value: res.append(record[0]) + return res async def subnet_exists( self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False @@ -785,9 +729,7 @@ async def neurons_lite( result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neurons_lite", - params=[ - netuid - ], + params=[netuid], block_hash=block_hash, reuse_block=reuse_block, ) @@ -952,8 +894,8 @@ async def fetch_coldkey_hotkey_identities( identities = {"coldkeys": {}, "hotkeys": {}} if not coldkey_identities: return identities - query = await self.substrate.query_multiple( - params=[(ss58) for ss58, _ in coldkey_identities.items()], + query = await self.substrate.query_multiple( # TODO probably more efficient to do this with query_multi + params=list(coldkey_identities.keys()), module="SubtensorModule", storage_function="OwnedHotkeys", block_hash=block_hash, @@ -994,7 +936,6 @@ async def 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. """ - # TODO look into seeing if we can speed this up with storage query w_map_encoded = await self.substrate.query_map( module="SubtensorModule", storage_function="Weights", @@ -1425,10 +1366,8 @@ async def get_stake_for_coldkeys( stake_info_map[coldkey_ss58] = StakeInfo.list_from_any(stake_info_list) return stake_info_map - async def all_subnets( - self, block_hash: Optional[str] = None - ) -> list[DynamicInfo]: - result = await self.substrate.runtime_call( + async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInfo]: + result = await self.query_runtime_api( "SubnetInfoRuntimeApi", "get_all_dynamic_info", block_hash=block_hash, @@ -1438,7 +1377,7 @@ async def all_subnets( async def subnet( self, netuid: int, block_hash: Optional[str] = None ) -> "DynamicInfo": - result = await self.substrate.runtime_call( + result = await self.query_runtime_api( "SubnetInfoRuntimeApi", "get_dynamic_info", params=[netuid], From b86cb7107296244fb3e1117f3e255efea140e6d6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 22:08:37 +0200 Subject: [PATCH 241/332] Fix type --- .../src/bittensor/subtensor_interface.py | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f5f9dac3..534e697b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -663,12 +663,12 @@ async def get_existential_deposit( The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. """ - result = await self.substrate.get_constant( + result = getattr(await self.substrate.get_constant( module_name="Balances", constant_name="ExistentialDeposit", block_hash=block_hash, reuse_block_hash=reuse_block, - ) + ), "value", None) if result is None: raise Exception("Unable to retrieve existential deposit amount.") @@ -1224,36 +1224,6 @@ async def get_delegate_identities( return all_delegates_details - async def get_delegates_by_netuid_light( - self, netuid: int, block_hash: Optional[str] = None - ) -> list[DelegateInfoLite]: - """ - Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the neurons that are actively involved in the network's delegation system. - - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of trust and responsibility among participating neurons. - - Args: - netuid: the netuid to query - block_hash: The hash of the blockchain block number for the query. - - Returns: - A list of DelegateInfo objects detailing each delegate's characteristics. - - """ - # TODO (Ben): doesn't exist - params = [netuid] if not block_hash else [netuid, block_hash] - json_body = await self.substrate.rpc_request( - method="delegateInfo_getDelegatesLight", # custom rpc method - params=params, - ) - - result = json_body["result"] - - if result in (None, []): - return [] - - return DelegateInfoLite.list_from_any(result) # TODO this won't work yet - async def get_stake_for_coldkey_and_hotkey_on_netuid( self, hotkey_ss58: str, From 11e76f2c93991d5713b7b60d846fbbcbcbaa2224 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 22:28:54 +0200 Subject: [PATCH 242/332] Fix NeuronInfoLite --- bittensor_cli/src/bittensor/chain_data.py | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 9a65324a..c5cc10ac 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -414,7 +414,7 @@ def get_null_neuron() -> "NeuronInfoLite": @classmethod def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite": active = decoded.get("active") - axon_info = decoded.get("axon_info") + axon_info = decoded.get("axon_info", {}) coldkey = decode_account_id(decoded.get("coldkey")) consensus = decoded.get("consensus") dividends = decoded.get("dividends") @@ -423,10 +423,10 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite incentive = decoded.get("incentive") last_update = decoded.get("last_update") netuid = decoded.get("netuid") - prometheus_info = decoded.get("prometheus_info") + prometheus_info = decoded.get("prometheus_info", {}) pruning_score = decoded.get("pruning_score") rank = decoded.get("rank") - stake_dict = process_stake_data(decoded.get("stake")) + stake_dict = process_stake_data(decoded.get("stake"), netuid) stake = sum(stake_dict.values()) if stake_dict else Balance(0) trust = decoded.get("trust") uid = decoded.get("uid") @@ -436,13 +436,13 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite neuron = cls( active=active, axon_info=AxonInfo( - version=axon_info.version, - ip=str(netaddr.IPAddress(axon_info.ip)), - port=axon_info.port, - ip_type=axon_info.ip_type, - placeholder1=axon_info.placeholder1, - placeholder2=axon_info.placeholder2, - protocol=axon_info.protocol, + version=axon_info.get("version"), + ip=str(netaddr.IPAddress(axon_info.get("ip"))), + port=axon_info.get("port"), + ip_type=axon_info.get("ip_type"), + placeholder1=axon_info.get("placeholder1"), + placeholder2=axon_info.get("placeholder2"), + protocol=axon_info.get("protocol"), hotkey=hotkey, coldkey=coldkey, ), @@ -455,11 +455,11 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite last_update=last_update, netuid=netuid, prometheus_info=PrometheusInfo( - version=prometheus_info.version, - ip=str(netaddr.IPAddress(prometheus_info.ip)), - port=prometheus_info.port, - ip_type=prometheus_info.ip_type, - block=prometheus_info.block, + version=prometheus_info.get("version"), + ip=str(netaddr.IPAddress(prometheus_info.get("ip"))), + port=prometheus_info.get("port"), + ip_type=prometheus_info.get("ip_type"), + block=prometheus_info.get("block"), ), pruning_score=pruning_score, rank=u16_normalized_float(rank), From eeccfe27cf382f39d8522f5a37f1e9a70fd13dbd Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 22:40:27 +0200 Subject: [PATCH 243/332] Ensure all _fix_decoded methods use `.get`, as this works on dicts and InfoBase classes --- bittensor_cli/src/bittensor/chain_data.py | 37 ++++++++++--------- .../src/bittensor/subtensor_interface.py | 16 +++++--- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index c5cc10ac..1884d7e1 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -309,21 +309,23 @@ def get_null_neuron() -> "NeuronInfo": @classmethod def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": - stake_dict = process_stake_data(decoded.stake) + netuid = decoded.get("netuid") + stake_dict = process_stake_data(decoded.get("stake"), netuid=netuid) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) - axon_info = decoded.axon_info + axon_info = decoded.get("axon_info", {}) coldkey = decode_account_id(decoded.get("coldkey")) hotkey = decode_account_id(decoded.get("hotkey")) + prometheus_info = decoded.get("prometheus_info", {}) return NeuronInfo( hotkey=hotkey, coldkey=coldkey, uid=decoded.get("uid"), - netuid=decoded.get("netuid"), + netuid=netuid, active=decoded.get("active"), stake=total_stake, stake_dict=stake_dict, total_stake=total_stake, - rank=u16_normalized_float(decoded.rget("ank")), + rank=u16_normalized_float(decoded.get("rank")), emission=decoded.get("emission") / 1e9, incentive=u16_normalized_float(decoded.get("incentive")), consensus=u16_normalized_float(decoded.get("consensus")), @@ -336,11 +338,11 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": bonds=[[e[0], e[1]] for e in decoded.get("bonds")], pruning_score=decoded.get("pruning_score"), prometheus_info=PrometheusInfo( - block=decoded.get("prometheus_info").get("block"), - version=decoded.get("prometheus_info").get("version"), - ip=str(netaddr.IPAddress(decoded.get("prometheus_info").get("ip"))), - port=decoded.get("prometheus_info").get("port"), - ip_type=decoded.get("prometheus_info").get("ip_type"), + block=prometheus_info.get("block"), + version=prometheus_info.get("version"), + ip=str(netaddr.IPAddress(prometheus_info.get("ip"))), + port=prometheus_info.get("port"), + ip_type=prometheus_info.get("ip_type"), ), axon_info=AxonInfo( version=axon_info.get("version"), @@ -508,10 +510,11 @@ class DelegateInfo(InfoBase): @classmethod def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": - hotkey = decode_account_id(decoded.hotkey_ss58) - owner = decode_account_id(decoded.owner_ss58) + hotkey = decode_account_id(decoded.get("hotkey_ss58")) + owner = decode_account_id(decoded.get("owner_ss58")) nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in decoded.nominators + (decode_account_id(x), Balance.from_rao(y)) + for x, y in decoded.get("nominators") ] total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) return cls( @@ -519,11 +522,11 @@ def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": total_stake=total_stake, nominators=nominators, owner_ss58=owner, - take=u16_normalized_float(decoded.take), - validator_permits=decoded.validator_permits, - registrations=decoded.registrations, - return_per_1000=Balance.from_rao(decoded.return_per_1000), - total_daily_return=Balance.from_rao(decoded.total_daily_return), + take=u16_normalized_float(decoded.get("take")), + validator_permits=decoded.get("validator_permits"), + registrations=decoded.get("registrations"), + return_per_1000=Balance.from_rao(decoded.get("return_per_1000")), + total_daily_return=Balance.from_rao(decoded.get("total_daily_return")), ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 534e697b..1f448d7b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -663,12 +663,16 @@ async def get_existential_deposit( The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. """ - result = getattr(await self.substrate.get_constant( - module_name="Balances", - constant_name="ExistentialDeposit", - block_hash=block_hash, - reuse_block_hash=reuse_block, - ), "value", None) + result = getattr( + await self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ), + "value", + None, + ) if result is None: raise Exception("Unable to retrieve existential deposit amount.") From 04c0f0ccdcf06da6aba45accd079cb803724b1e6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 22:54:11 +0200 Subject: [PATCH 244/332] Remove PrometheusInfo --- bittensor_cli/src/bittensor/chain_data.py | 75 ++++++----------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 1884d7e1..8c2875c3 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -218,24 +218,6 @@ def _fix_decoded(cls, decoded: Any) -> "StakeInfo": ) -@dataclass -class PrometheusInfo(InfoBase): - """Dataclass for prometheus info.""" - - block: int - version: int - ip: str - port: int - ip_type: int - - @classmethod - def _fix_decoded(cls, decoded: Any) -> "PrometheusInfo": - """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" - decoded["ip"] = int_to_ip(int(decoded["ip"])) - - return cls(**decoded) - - @dataclass class NeuronInfo(InfoBase): """Dataclass for neuron metadata.""" @@ -261,7 +243,6 @@ class NeuronInfo(InfoBase): weights: list[list[int]] bonds: list[list[int]] pruning_score: int - prometheus_info: Optional["PrometheusInfo"] = None axon_info: Optional[AxonInfo] = None is_null: bool = False @@ -298,7 +279,6 @@ def get_null_neuron() -> "NeuronInfo": validator_permit=False, weights=[], bonds=[], - prometheus_info=None, axon_info=None, is_null=True, coldkey="000000000000000000000000000000000000000000000000", @@ -315,7 +295,6 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": axon_info = decoded.get("axon_info", {}) coldkey = decode_account_id(decoded.get("coldkey")) hotkey = decode_account_id(decoded.get("hotkey")) - prometheus_info = decoded.get("prometheus_info", {}) return NeuronInfo( hotkey=hotkey, coldkey=coldkey, @@ -337,13 +316,6 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": weights=[[e[0], e[1]] for e in decoded.get("weights")], bonds=[[e[0], e[1]] for e in decoded.get("bonds")], pruning_score=decoded.get("pruning_score"), - prometheus_info=PrometheusInfo( - block=prometheus_info.get("block"), - version=prometheus_info.get("version"), - ip=str(netaddr.IPAddress(prometheus_info.get("ip"))), - port=prometheus_info.get("port"), - ip_type=prometheus_info.get("ip_type"), - ), axon_info=AxonInfo( version=axon_info.get("version"), ip=str(netaddr.IPAddress(axon_info.get("ip"))), @@ -381,7 +353,6 @@ class NeuronInfoLite(InfoBase): dividends: float last_update: int validator_permit: bool - prometheus_info: Optional["PrometheusInfo"] axon_info: AxonInfo pruning_score: int is_null: bool = False @@ -404,7 +375,6 @@ def get_null_neuron() -> "NeuronInfoLite": dividends=0, last_update=0, validator_permit=False, - prometheus_info=None, axon_info=None, is_null=True, coldkey="000000000000000000000000000000000000000000000000", @@ -425,7 +395,6 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite incentive = decoded.get("incentive") last_update = decoded.get("last_update") netuid = decoded.get("netuid") - prometheus_info = decoded.get("prometheus_info", {}) pruning_score = decoded.get("pruning_score") rank = decoded.get("rank") stake_dict = process_stake_data(decoded.get("stake"), netuid) @@ -456,13 +425,6 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite incentive=u16_normalized_float(incentive), last_update=last_update, netuid=netuid, - prometheus_info=PrometheusInfo( - version=prometheus_info.get("version"), - ip=str(netaddr.IPAddress(prometheus_info.get("ip"))), - port=prometheus_info.get("port"), - ip_type=prometheus_info.get("ip_type"), - block=prometheus_info.get("block"), - ), pruning_score=pruning_score, rank=u16_normalized_float(rank), stake_dict=stake_dict, @@ -595,29 +557,28 @@ class SubnetInfo(InfoBase): @classmethod def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": - d = decoded return SubnetInfo( - netuid=d.netuid, - rho=d.rho, - kappa=d.kappa, - difficulty=d.difficulty, - immunity_period=d.immunity_period, - max_allowed_validators=d.max_allowed_validators, - min_allowed_weights=d.min_allowed_weights, - max_weight_limit=d.max_weights_limit, - scaling_law_power=d.scaling_law_power, - subnetwork_n=d.subnetwork_n, - max_n=d.max_allowed_uids, - blocks_since_epoch=d.blocks_since_last_step, - tempo=d.tempo, - modality=d.network_modality, + netuid=decoded.get("netuid"), + rho=decoded.get("rho"), + kappa=decoded.get("kappa"), + difficulty=decoded.get("difficulty"), + immunity_period=decoded.get("immunity_period"), + max_allowed_validators=decoded.get("max_allowed_validators"), + min_allowed_weights=decoded.get("min_allowed_weights"), + max_weight_limit=decoded.get("max_weights_limit"), + scaling_law_power=decoded.get("scaling_law_power"), + subnetwork_n=decoded.get("subnetwork_n"), + max_n=decoded.get("max_allowed_uids"), + blocks_since_epoch=decoded.get("blocks_since_last_step"), + tempo=decoded.get("tempo"), + modality=decoded.get("network_modality"), connection_requirements={ str(int(netuid)): u16_normalized_float(int(req)) - for (netuid, req) in d.network_connect + for (netuid, req) in decoded.get("network_connect") }, - emission_value=d.emission_values, - burn=Balance.from_rao(d.burn), - owner_ss58=decode_account_id(d.owner), + emission_value=decoded.get("emission_values"), + burn=Balance.from_rao(decoded.get("burn")), + owner_ss58=decode_account_id(decoded.get("owner")), ) From 74f4366c959886c17b623c22e4f8224e67b9101b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 23:00:17 +0200 Subject: [PATCH 245/332] Remove substrateinterface entirely. --- bittensor_cli/src/bittensor/extrinsics/registration.py | 2 +- bittensor_cli/src/bittensor/extrinsics/root.py | 2 +- bittensor_cli/src/bittensor/extrinsics/transfer.py | 2 +- bittensor_cli/src/bittensor/subtensor_interface.py | 3 +-- bittensor_cli/src/commands/stake/children_hotkeys.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 2 +- bittensor_cli/src/commands/subnets/subnets.py | 2 +- bittensor_cli/src/commands/weights.py | 2 +- requirements.txt | 1 - 9 files changed, 8 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 7eb10a1a..4759031a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -26,7 +26,7 @@ from rich.prompt import Confirm from rich.console import Console from rich.status import Status -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import NeuronInfo diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index d5df6eed..1c21f06c 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -27,7 +27,7 @@ from rich.prompt import Confirm from rich.table import Table, Column from scalecodec import ScaleBytes, U16, Vec -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.extrinsics.registration import is_hotkey_registered diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 790eaa64..f5a2b3db 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -3,7 +3,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import NETWORK_EXPLORER_MAP from bittensor_cli.src.bittensor.balances import Balance diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 1f448d7b..16840983 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -4,9 +4,8 @@ import aiohttp from bittensor_wallet import Wallet from bittensor_wallet.utils import SS58_FORMAT -import scalecodec from scalecodec import GenericCall -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException import typer diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 053218bc..c9ae6487 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -6,7 +6,7 @@ from rich.prompt import Confirm, Prompt, IntPrompt from rich.table import Table from rich.text import Text -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 3af7dfde..1bc3f047 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -12,7 +12,7 @@ from rich.progress import Progress, BarColumn, TextColumn from rich.console import Group from rich.live import Live -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 6f4acae1..73b30c09 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -76,7 +76,7 @@ async def _find_event_attributes_in_extrinsic_receipt( """ Searches for the attributes of a specified event within an extrinsic receipt. - :param response_: (substrateinterface.base.ExtrinsicReceipt): The receipt of the extrinsic to be searched. + :param response_: The receipt of the extrinsic to be searched. :param event_name: The name of the event to search for. :return: A list of attributes for the specified event. Returns [-1] if the event is not found. diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index cc5a7d37..caf5e72f 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -7,7 +7,7 @@ import numpy as np from numpy.typing import NDArray from rich.prompt import Confirm -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.utils import err_console, console, format_error_message from bittensor_cli.src.bittensor.extrinsics.root import ( diff --git a/requirements.txt b/requirements.txt index e21d8a2c..decf2e88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,6 @@ pytest python-Levenshtein rich~=13.7 scalecodec==1.2.11 -substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 bittensor-wallet>=3.0.0 From 65725adb9cd98e1d9074ba02234e8747115e675c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 23:17:14 +0200 Subject: [PATCH 246/332] Clean up --- bittensor_cli/cli.py | 4 +- bittensor_cli/src/bittensor/balances.py | 10 +---- .../src/bittensor/extrinsics/registration.py | 4 +- .../src/bittensor/subtensor_interface.py | 5 +-- bittensor_cli/src/bittensor/utils.py | 45 +------------------ bittensor_cli/src/commands/stake/__init__.py | 5 +-- bittensor_cli/src/commands/stake/move.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 1 + bittensor_cli/src/commands/subnets/subnets.py | 3 +- bittensor_cli/src/commands/wallets.py | 9 ++-- 10 files changed, 18 insertions(+), 70 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 01e29b3a..f17b836d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1489,8 +1489,8 @@ def wallet_transfer( validate=WV.WALLET, ) - # For Rao games - temporarilyt commented out - effective_network = get_effective_network(self.config, network) + # For Rao games - temporarily commented out + # effective_network = get_effective_network(self.config, network) # if is_rao_network(effective_network): # print_error("This command is disabled on the 'rao' network.") # raise typer.Exit() diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index f1bb90fb..3a9c2fc1 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -17,7 +17,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union, TypedDict +from typing import Union from bittensor_cli.src import UNITS @@ -229,12 +229,6 @@ def __rfloordiv__(self, other: Union[int, float, "Balance"]): except (ValueError, TypeError): raise NotImplementedError("Unsupported type") - def __int__(self) -> int: - return self.rao - - def __float__(self) -> float: - return self.tao - def __nonzero__(self) -> bool: return bool(self.rao) @@ -306,7 +300,7 @@ def set_unit(self, netuid: int): def fixed_to_float(fixed: dict) -> float: # Currently this is stored as a U64F64 # which is 64 bits of integer and 64 bits of fractional - uint_bits = 64 + # uint_bits = 64 frac_bits = 64 data: int = fixed["bits"] diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 4759031a..dc4d00ca 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -1022,7 +1022,7 @@ async def _block_solver( limit = int(math.pow(2, 256)) - 1 # Establish communication queues - ## See the _Solver class for more information on the queues. + # See the _Solver class for more information on the queues. stop_event = Event() stop_event.clear() @@ -1035,7 +1035,7 @@ async def _block_solver( ) if cuda: - ## Create a worker per CUDA device + # Create a worker per CUDA device num_processes = len(dev_id) solvers = [ _CUDASolver( diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 16840983..3ee0a7f1 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -18,7 +18,6 @@ SubnetHyperparameters, decode_account_id, decode_hex_identity, - DelegateInfoLite, DynamicInfo, SubnetState, ) @@ -55,11 +54,11 @@ def __init__(self, proposal_dict: dict) -> None: self.end = proposal_dict["end"] @staticmethod - def decode_ss58_tuples(l: tuple): + def decode_ss58_tuples(data: tuple): """ Decodes a tuple of ss58 addresses formatted as bytes tuples """ - return [decode_account_id(l[x][0]) for x in range(len(l))] + return [decode_account_id(data[x][0]) for x in range(len(data))] class SubtensorInterface: diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3a458ca1..5e8091a7 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -15,16 +15,12 @@ from bittensor_wallet.utils import SS58_FORMAT from bittensor_wallet.errors import KeyFileError from bittensor_wallet import utils -import bt_decode from jinja2 import Template from markupsafe import Markup import numpy as np from numpy.typing import NDArray from rich.console import Console from rich.prompt import Prompt -import scalecodec -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -376,45 +372,6 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_scale_bytes( - return_type: str, - scale_bytes: Union["scalecodec.ScaleBytes", bytes], - custom_rpc_type_registry: Union[str, "bt_decode.PortableRegistry"], -) -> Any: - """ - Decodes a ScaleBytes object using our type registry and return type - - :param return_type: the type string to decode the scale bytes to - :param scale_bytes: the scale bytes to decode (either a scalecodec.ScaleBytes or bytes) - :param custom_rpc_type_registry: contains the type registry - - :return: the decoded object - """ - if isinstance(custom_rpc_type_registry, str): - portable_registry = bt_decode.PortableRegistry.from_json( - custom_rpc_type_registry - ) - else: - portable_registry = custom_rpc_type_registry - - if isinstance(scale_bytes, scalecodec.ScaleBytes): - as_bytes = bytes(scale_bytes.data) - else: - as_bytes = bytes(scale_bytes) - - if as_bytes.hex() == "0x0400": # RPC returned None result - return None - - return bt_decode.decode(return_type, portable_registry, as_bytes) - - -def bytes_from_hex_string_result(hex_string_result: str) -> bytes: - if hex_string_result.startswith("0x"): - hex_string_result = hex_string_result[2:] - - return bytes.fromhex(hex_string_result) - - def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]): if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): account_id_bytes = account_id_bytes[0] @@ -1216,7 +1173,7 @@ def is_valid_github_url(url: str) -> bool: return False return True - except: + except Exception: # TODO figure out the exceptions that can be raised in here return False diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index 6b0b0fc9..d1dceeab 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -3,9 +3,8 @@ import rich.prompt from rich.table import Table -from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.chain_data import DelegateInfo, DelegateInfoLite -from bittensor_cli.src.bittensor.utils import console, err_console +from bittensor_cli.src.bittensor.chain_data import DelegateInfoLite +from bittensor_cli.src.bittensor.utils import console if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index dcf9e3a0..d6ad378c 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -648,7 +648,7 @@ async def move_stake( if not await response.is_success: err_console.print( f"\n:cross_mark: [red]Failed[/red] with error:" - f" {format_error_message( await response.error_message, subtensor.substrate)}" + f" {format_error_message(await response.error_message)}" ) return else: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 1bc3f047..d1f6289c 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1343,6 +1343,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: if issuance.tao != 0: + # TODO figure out why this alpha_ownership does nothing alpha_ownership = "{:.4f}".format( (alpha_value.tao / issuance.tao) * 100 ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 73b30c09..e5558f36 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -14,7 +14,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, @@ -974,7 +973,7 @@ async def show_root(): The table displays the root subnet participants and their metrics. The columns are as follows: - Position: The sorted position of the hotkey by total TAO. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. - Stake: The stake balance of this hotkey on root (measured in TAO). - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - Hotkey: The hotkey ss58 address. diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b664dcbb..81fd75f6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2,7 +2,7 @@ import itertools import os from collections import defaultdict -from typing import Any, Generator, Optional +from typing import Generator, Optional import aiohttp from bittensor_wallet import Wallet, Keypair @@ -14,7 +14,6 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding -from scalecodec import ScaleBytes import typer from bittensor_cli.src import COLOR_PALETTE @@ -1029,7 +1028,7 @@ def _map_hotkey_to_neurons( async def _fetch_neuron_for_netuid( netuid: int, subtensor: SubtensorInterface -) -> tuple[int, dict[str, list[ScaleBytes]]]: +) -> tuple[int, list[NeuronInfoLite]]: """ Retrieves all neurons for a specified netuid @@ -1044,7 +1043,7 @@ async def _fetch_neuron_for_netuid( async def _fetch_all_neurons( netuids: list[int], subtensor -) -> list[tuple[int, list[ScaleBytes]]]: +) -> list[tuple[int, list[NeuronInfoLite]]]: """Retrieves all neurons for each of the specified netuids""" return list( await asyncio.gather( @@ -1054,7 +1053,7 @@ async def _fetch_all_neurons( def _process_neurons_for_netuids( - netuids_with_all_neurons_hex_bytes: list[tuple[int, list[ScaleBytes]]], + netuids_with_all_neurons_hex_bytes: list[tuple[int, list[NeuronInfoLite]]], ) -> list[tuple[int, list[NeuronInfoLite]]]: """ Using multiprocessing to decode a list of hex-bytes neurons with their respective netuid From a4c464c721a068152689edf0f31f8e5cf3efbe64 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 30 Jan 2025 23:20:24 +0200 Subject: [PATCH 247/332] Async Substrate Bump --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index decf2e88..a8a70c10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface==1.0.0rc7 +async-substrate-interface==1.0.0rc8 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 From d2c2aa5e7998b408de85fced9ac51b0df84c8b52 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 30 Jan 2025 15:18:16 -0800 Subject: [PATCH 248/332] bumps async substrate interface --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8a70c10..4b903f3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface==1.0.0rc8 +async-substrate-interface==1.0.0rc9 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 From df09d77cde77d13666076e6f3fef3d2e2a361259 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 30 Jan 2025 18:09:27 -0800 Subject: [PATCH 249/332] Update sn_owner call sudo_get --- bittensor_cli/src/commands/sudo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 20b50605..ec7a6f78 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -95,12 +95,11 @@ async def set_hyperparameter_extrinsic( finalization/inclusion, the response is `True`. """ print_verbose("Confirming subnet owner") - subnet_owner_ = await subtensor.query( + subnet_owner = await subtensor.query( module="SubtensorModule", storage_function="SubnetOwner", params=[netuid], ) - subnet_owner = decode_account_id(subnet_owner_[0]) if subnet_owner != wallet.coldkeypub.ss58_address: err_console.print( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" From d282aff2501bc502692fce3ad88e026c6d049659 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 30 Jan 2025 18:15:51 -0800 Subject: [PATCH 250/332] Updates fetching account ids --- bittensor_cli/src/bittensor/subtensor_interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 3ee0a7f1..7a546e50 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -995,27 +995,27 @@ async def does_hotkey_exist( :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ - _result = await self.query( + result = await self.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - result = decode_account_id(_result[0]) # TODO verify return_val = result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" return return_val async def get_hotkey_owner( - self, hotkey_ss58: str, block_hash: str + self, + hotkey_ss58: str, + block_hash: Optional[str] = None, ) -> Optional[str]: - hk_owner_query = await self.query( + val = await self.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, ) - val = decode_account_id(hk_owner_query[0]) if val: exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) else: From 1d0b33bcc2004c72fbbf15be9343f929b3e96a5f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 31 Jan 2025 13:03:57 +0200 Subject: [PATCH 251/332] Normalise `max_weights_limit` name to coincide with the chain data. --- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index d775b546..00021ba7 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -633,7 +633,7 @@ class WalletValidationTypes(Enum): "max_difficulty": "sudo_set_max_difficulty", "weights_version": "sudo_set_weights_version_key", "weights_rate_limit": "sudo_set_weights_set_rate_limit", - "max_weight_limit": "sudo_set_max_weight_limit", + "max_weights_limit": "sudo_set_max_weight_limit", "immunity_period": "sudo_set_immunity_period", "min_allowed_weights": "sudo_set_min_allowed_weights", "activity_cutoff": "sudo_set_activity_cutoff", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 8c2875c3..7ce174b0 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -128,7 +128,7 @@ class SubnetHyperparameters(InfoBase): kappa: int immunity_period: int min_allowed_weights: int - max_weight_limit: float + max_weights_limit: float tempo: int min_difficulty: int max_difficulty: int @@ -161,7 +161,7 @@ def _fix_decoded( kappa=decoded.get("kappa"), immunity_period=decoded.get("immunity_period"), min_allowed_weights=decoded.get("min_allowed_weights"), - max_weight_limit=decoded.get("max_weights_limit"), + max_weights_limit=decoded.get("max_weights_limit"), tempo=decoded.get("tempo"), min_difficulty=decoded.get("min_difficulty"), max_difficulty=decoded.get("max_difficulty"), @@ -543,7 +543,7 @@ class SubnetInfo(InfoBase): immunity_period: int max_allowed_validators: int min_allowed_weights: int - max_weight_limit: float + max_weights_limit: float scaling_law_power: float subnetwork_n: int max_n: int @@ -565,7 +565,7 @@ def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": immunity_period=decoded.get("immunity_period"), max_allowed_validators=decoded.get("max_allowed_validators"), min_allowed_weights=decoded.get("min_allowed_weights"), - max_weight_limit=decoded.get("max_weights_limit"), + max_weights_limit=decoded.get("max_weights_limit"), scaling_law_power=decoded.get("scaling_law_power"), subnetwork_n=decoded.get("subnetwork_n"), max_n=decoded.get("max_allowed_uids"), From fa116118ebe03a75941a593639da1391b867d36e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 31 Jan 2025 15:02:44 +0200 Subject: [PATCH 252/332] Arg renaming --- bittensor_cli/src/bittensor/chain_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 7ce174b0..20e1f7a1 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -106,12 +106,12 @@ def _fix_decoded(self, decoded: Any) -> "InfoBase": ) @classmethod - def from_any(cls, any_: Any) -> "InfoBase": - return cls._fix_decoded(any_) + def from_any(cls, data: Any) -> "InfoBase": + return cls._fix_decoded(data) @classmethod - def list_from_any(cls, any_list: list[Any]) -> list["InfoBase"]: - return [cls.from_any(any_) for any_ in any_list] + def list_from_any(cls, data_list: list[Any]) -> list["InfoBase"]: + return [cls.from_any(data) for data in data_list] def __getitem__(self, item): return getattr(self, item) From f25ca6620457b265ff90bc06eaf2c4e91c14d6f2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 31 Jan 2025 15:39:37 +0200 Subject: [PATCH 253/332] Remove trailing slash --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 00021ba7..a3379ef0 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -8,7 +8,7 @@ class Constants: finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443" archive_entrypoint = "wss://archive.chain.opentensor.ai:443" - rao_entrypoint = "wss://rao.chain.opentensor.ai:443/" + rao_entrypoint = "wss://rao.chain.opentensor.ai:443" dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " local_entrypoint = "ws://127.0.0.1:9944" network_map = { From 0cc566ccfaa5d3051b5831bef2cac70efb535434 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 31 Jan 2025 15:47:55 +0200 Subject: [PATCH 254/332] Add in network debug info --- bittensor_cli/src/bittensor/subtensor_interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7a546e50..9e3fd8cc 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -91,7 +91,8 @@ def __init__(self, network): f"Network not specified or not valid. Using default chain endpoint: " f"{Constants.network_map[defaults.subtensor.network]}.\n" f"You can set this for commands with the `--network` flag, or by setting this" - f" in the config." + f" in the config. If you're sure you're using the correct URL, ensure it begins" + f" with `ws://` or `wss://`" ) self.chain_endpoint = Constants.network_map[defaults.subtensor.network] self.network = defaults.subtensor.network From b15203d382c1e7d7d16e8e22fee4dcb4907e7ff1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 31 Jan 2025 15:48:48 +0200 Subject: [PATCH 255/332] Add in network debug info --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9e3fd8cc..39ea95d4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -92,7 +92,7 @@ def __init__(self, network): f"{Constants.network_map[defaults.subtensor.network]}.\n" f"You can set this for commands with the `--network` flag, or by setting this" f" in the config. If you're sure you're using the correct URL, ensure it begins" - f" with `ws://` or `wss://`" + f" with 'ws://' or 'wss://'" ) self.chain_endpoint = Constants.network_map[defaults.subtensor.network] self.network = defaults.subtensor.network From 543455448d278ce4365b58d3911b14c9e7791f50 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 02:08:24 +0200 Subject: [PATCH 256/332] Typing --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 39ea95d4..9766d358 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -363,7 +363,7 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ sub_stakes = await self.get_stake_for_coldkeys( - ss58_addresses, block_hash=block_hash + list(ss58_addresses), block_hash=block_hash ) # Token pricing info dynamic_info = await self.all_subnets() @@ -1327,7 +1327,7 @@ async def get_stake_for_coldkeys( result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkeys", - params=[coldkey_ss58_list], + params=coldkey_ss58_list, block_hash=block_hash, ) if result is None: From c489a9fc11f10a8cb1115673b50a2c57c5284ef5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 02:46:39 +0200 Subject: [PATCH 257/332] Cleaner error-handling with verbose error tracebacks. --- bittensor_cli/cli.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f17b836d..b37638bb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5,6 +5,7 @@ import re import ssl import sys +import traceback from pathlib import Path from typing import Coroutine, Optional from dataclasses import fields @@ -857,23 +858,38 @@ def _run_command(self, cmd: Coroutine) -> None: """ async def _run(): + initiated = False try: if self.subtensor: async with self.subtensor: + initiated = True result = await cmd else: + initiated = True result = await cmd return result except (ConnectionRefusedError, ssl.SSLError): err_console.print(f"Unable to connect to the chain: {self.subtensor}") - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except ConnectionClosed: - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except SubstrateRequestException as e: - err_console.print(str(e)) - raise typer.Exit() + verbose_console.print(traceback.format_exc()) + except ( + ConnectionClosed, + SubstrateRequestException, + KeyboardInterrupt, + ) as e: + if isinstance(e, SubstrateRequestException): + err_console.print(str(e)) + verbose_console.print(traceback.format_exc()) + except Exception as e: + err_console.print(f"An unknown error has occurred: {e}") + verbose_console.print(traceback.format_exc()) + finally: + if initiated is False: + asyncio.create_task(cmd).cancel() + try: + raise typer.Exit() + except Exception as e: # ensures we always exit cleanly + err_console.print(f"An unknown error has occurred: {e}") + sys.exit() if sys.version_info < (3, 10): # For Python 3.9 or lower From f45b7c4db9afec6cc58d5d4b0edff9b0a46fa017 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 16:10:38 +0200 Subject: [PATCH 258/332] Docstring standardisation. --- bittensor_cli/cli.py | 6 +- .../src/bittensor/subtensor_interface.py | 57 ++++++++----------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b37638bb..90438833 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -872,9 +872,9 @@ async def _run(): err_console.print(f"Unable to connect to the chain: {self.subtensor}") verbose_console.print(traceback.format_exc()) except ( - ConnectionClosed, - SubstrateRequestException, - KeyboardInterrupt, + ConnectionClosed, + SubstrateRequestException, + KeyboardInterrupt, ) as e: if isinstance(e, SubstrateRequestException): err_console.print(str(e)) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9766d358..caca2446 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -158,6 +158,7 @@ async def get_all_subnet_netuids( Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. :param block_hash: The hash of the block to retrieve the subnet unique identifiers from. + :return: A list of subnet netuids. This function provides a comprehensive view of the subnets within the Bittensor network, @@ -218,14 +219,13 @@ async def get_stake_for_coldkey_and_hotkey( """ Returns the stake under a coldkey - hotkey pairing. - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - coldkey_ss58 (str): The SS58 address of the coldkey. - netuid (Optional[int]): The subnet ID to filter by. If provided, only returns stake for this specific subnet. - block_hash (Optional[str]): The block hash at which to query the stake information. + :param hotkey_ss58: The SS58 address of the hotkey. + :param coldkey_ss58: The SS58 address of the coldkey. + :param netuid: The subnet ID to filter by. If provided, only returns stake for this specific + subnet. + :param block_hash: The block hash at which to query the stake information. - Returns: - Balance: The stake under the coldkey - hotkey pairing. + :return: Balance: The stake under the coldkey - hotkey pairing. """ alpha_shares = await self.query( module="SubtensorModule", @@ -456,13 +456,11 @@ async def current_take( 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. - Args: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block_hash: The hash of the block number to retrieve the stake from. - reuse_block: Whether to reuse the last-used block hash when retrieving info. + :param hotkey_ss58: The `SS58` address of the neuron's hotkey. + :param block_hash: The hash of the block number to retrieve the stake from. + :param reuse_block: Whether to reuse the last-used block hash when retrieving info. - Returns: - The delegate take percentage, None if not available. + :return: The delegate take percentage, None if not available. The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. @@ -540,12 +538,10 @@ async def get_subnet_state( """ Retrieves the state of a specific subnet within the Bittensor network. - Args: - netuid: The network UID of the subnet to query. - block_hash: The hash of the blockchain block number for the query. + :param netuid: The network UID of the subnet to query. + :param block_hash: The hash of the blockchain block number for the query. - Returns: - SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. + :return: SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. """ result = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", @@ -930,7 +926,6 @@ async def weights( This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust and value assignment mechanisms. - Args: :param netuid: The network UID of the subnet to query. :param block_hash: The hash of the blockchain block for the query. @@ -1168,10 +1163,9 @@ async def get_delegate_identities( is filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from GitHub, but chain data is still limited in that regard. - Args: - block_hash: the hash of the blockchain block for the query + :param block_hash: the hash of the blockchain block for the query - Returns: {ss58: DelegatesDetails, ...} + :return: {ss58: DelegatesDetails, ...} """ timeout = aiohttp.ClientTimeout(10.0) @@ -1256,13 +1250,12 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( """ Queries the stake for multiple hotkey - coldkey - netuid pairings. - Args: - hotkey_ss58s: list of hotkey ss58 addresses - coldkey_ss58: a single coldkey ss58 address - netuids: list of netuids - block_hash: hash of the blockchain block, if any + :param hotkey_ss58s: list of hotkey ss58 addresses + :param coldkey_ss58: a single coldkey ss58 address + :param netuids: list of netuids + :param block_hash: hash of the blockchain block, if any - Returns: + :return: { hotkey_ss58_1: { netuid_1: netuid1_stake, @@ -1314,12 +1307,10 @@ async def get_stake_for_coldkeys( Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple accounts, providing a collective view of their stakes and delegations. - Args: - coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys. - block_hash: The blockchain block number for the query. + :param coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys. + :param block_hash: The blockchain block number for the query. - Returns: - A dictionary mapping each coldkey to a list of its StakeInfo objects. + :return: A dictionary mapping each coldkey to a list of its StakeInfo objects. This function is useful for analyzing the stake distribution and delegation patterns of multiple accounts simultaneously, offering a broader perspective on network participation and investment strategies. From 527045785f73f0a35101b899be9d9148b6a32f76 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 17:04:02 +0200 Subject: [PATCH 259/332] Added --commands help section. --- bittensor_cli/cli.py | 64 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 90438833..cae32d77 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -17,6 +17,7 @@ from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table +from rich.tree import Tree from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -58,6 +59,7 @@ try: from git import Repo, GitError except ImportError: + Repo = None class GitError(Exception): pass @@ -278,7 +280,8 @@ def parse_to_list( def verbosity_console_handler(verbosity_level: int = 1) -> None: """ Sets verbosity level of console output - :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is verbose) + :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is + verbose) """ if verbosity_level not in range(3): raise ValueError( @@ -309,7 +312,8 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", + f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", default=None, show_default=False, ) @@ -472,6 +476,13 @@ def version_callback(value: bool): raise typer.Exit() +def commands_callback(value: bool): + if value: + cli = CLIManager() + console.print(cli.generate_command_tree()) + raise typer.Exit() + + class CLIManager: """ :var app: the main CLI Typer app @@ -816,6 +827,40 @@ def __init__(self): self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + def generate_command_tree(self) -> Tree: + """ + Generates a rich.Tree of the commands, subcommands, and groups of this app + """ + + def build_rich_tree(data: dict, parent: Tree): + for group, content in data.get("groups", {}).items(): + group_node = parent.add( + f"[bold cyan]{group}[/]" + ) # Add group to the tree + for command in content.get("commands", []): + group_node.add(f"[green]{command}[/]") # Add commands to the group + build_rich_tree(content, group_node) # Recurse for subgroups + + def traverse_group(group: typer.Typer) -> dict: + tree = {} + if commands := [ + cmd.name for cmd in group.registered_commands if not cmd.hidden + ]: + tree["commands"] = commands + for group in group.registered_groups: + if "groups" not in tree: + tree["groups"] = {} + if not group.hidden: + if group_transversal := traverse_group(group.typer_instance): + tree["groups"][group.name] = group_transversal + + return tree + + groups_and_commands = traverse_group(self.app) + root = Tree("[bold magenta]BTCLI Commands[/]") # Root node + build_rich_tree(groups_and_commands, root) + return root + def initialize_chain( self, network: Optional[list[str]] = None, @@ -901,13 +946,22 @@ async def _run(): def main_callback( self, version: Annotated[ - Optional[bool], typer.Option("--version", callback=version_callback) + Optional[bool], + typer.Option( + "--version", callback=version_callback, help="Show BTCLI version" + ), + ] = None, + commands: Annotated[ + Optional[bool], + typer.Option( + "--commands", callback=commands_callback, help="Show BTCLI commands" + ), ] = None, ): """ - Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be overriden by passing them explicitly in the command line. + Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be + overriden by passing them explicitly in the command line. """ - # Load or create the config file if os.path.exists(self.config_path): with open(self.config_path, "r") as f: From 77f54d134ac4541772b32371525ceb158d8fab8c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 17:31:48 +0200 Subject: [PATCH 260/332] Docstring --- bittensor_cli/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cae32d77..f8e2ab49 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -477,6 +477,9 @@ def version_callback(value: bool): def commands_callback(value: bool): + """ + Prints a tree of commands for the app + """ if value: cli = CLIManager() console.print(cli.generate_command_tree()) From 398791fcb10840368c73c0db48c681e7ab2503b1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 09:01:34 -0800 Subject: [PATCH 261/332] working --- bittensor_cli/cli.py | 3 +- bittensor_cli/src/commands/stake/stake.py | 8 ++- bittensor_cli/src/commands/subnets/subnets.py | 60 +++++++++---------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7bd37fbb..7393ee8e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2478,6 +2478,7 @@ def stake_list( live: bool = Options.live, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + no_prompt: bool = Options.prompt, # TODO add: all-wallets, reuse_last, html_output ): """List all stake accounts for wallet.""" @@ -2506,7 +2507,7 @@ def stake_list( return self._run_command( stake.stake_list( - wallet, coldkey_ss58, self.initialize_chain(network), live, verbose + wallet, coldkey_ss58, self.initialize_chain(network), live, verbose, no_prompt ) ) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index bd95a0e5..b6fe73e4 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1185,6 +1185,7 @@ async def stake_list( subtensor: "SubtensorInterface", live: bool = False, verbose: bool = False, + prompt: bool = False, ): coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address @@ -1217,8 +1218,9 @@ def define_table( live: bool = False, ): title = f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {hotkey_name}\nNetwork: {subtensor.network}\n\n" - if not live: - title += f"[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n" + # TODO: Add hint back in after adding columns descriptions + # if not live: + # title += f"[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n" table = Table( title=title, show_footer=True, @@ -1729,7 +1731,7 @@ def format_cell( all_hotkeys_total_global_tao += stake all_hotkeys_total_tao_value += value - if num_hotkeys > 1 and counter < num_hotkeys: + if num_hotkeys > 1 and counter < num_hotkeys and prompt: console.print("\nPress Enter to continue to the next hotkey...") input() diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 88fb63ec..c271c430 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -983,33 +983,31 @@ async def show_root(): """ ) if delegate_selection: + valid_uids = [str(row[0]) for row in sorted_rows[:max_rows]] while True: selection = Prompt.ask( - "\nEnter the position of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + "\nEnter the Position of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", default="", + choices=[""] + valid_uids, + show_choices=False, + show_default=False, ) if selection == "": return None - - try: - idx = int(selection) - if 1 <= idx <= max_rows: - selected_hotkey = sorted_hks_delegation[idx - 1] - row_data = sorted_rows[idx - 1] - identity = "" if row_data[5] == "~" else row_data[5] - identity_str = f" ({identity})" if identity else "" - console.print( - f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" - ) - - return selected_hotkey - else: - console.print( - f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]" - ) - except ValueError: - console.print("[red]Please enter a valid number[/red]") + + uid = int(selection) + selected_hotkey = root_state.hotkeys[uid] + # Find identity for this UID + coldkey_identity = identities.get(root_state.coldkeys[uid], {}).get("name", "") + hotkey_identity = old_identities.get(selected_hotkey) + validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") + identity_str = f" ({validator_identity})" if validator_identity else "" + + console.print( + f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" + ) + return selected_hotkey async def show_subnet(netuid_: int): if not await subtensor.subnet_exists(netuid=netuid): @@ -1053,10 +1051,6 @@ async def show_subnet(netuid_: int): pad_edge=True, ) - # Add index for selection if selecting delegates - if delegate_selection: - table.add_column("#", style="cyan", justify="right") - # For hotkey_block_emission calculation emission_sum = sum( [ @@ -1247,8 +1241,6 @@ async def show_subnet(netuid_: int): ) for pos, row in enumerate(rows, 1): table_row = [] - if delegate_selection: - table_row.append(str(pos)) table_row.extend(row) table.add_row(*table_row) if delegate_selection and pos == max_rows: @@ -1301,20 +1293,24 @@ async def show_subnet(netuid_: int): if delegate_selection: while True: + valid_uids = [str(row[0]) for row in rows[:max_rows]] selection = Prompt.ask( - "\nEnter the number of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + "\nEnter the UID of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", default="", + choices=[""] + valid_uids, + show_choices=False, + show_default=False, ) if selection == "": return None try: - idx = int(selection) - if 1 <= idx <= max_rows: - uid = int(rows[idx - 1][0]) + uid = int(selection) + # Check if the UID exists in the subnet + if uid in [int(row[0]) for row in rows]: + row_data = next(row for row in rows if int(row[0]) == uid) hotkey = subnet_state.hotkeys[uid] - row_data = rows[idx - 1] identity = "" if row_data[9] == "~" else row_data[9] identity_str = f" ({identity})" if identity else "" console.print( @@ -1323,7 +1319,7 @@ async def show_subnet(netuid_: int): return hotkey else: console.print( - f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]" + f"[red]Invalid UID. Please enter a valid UID from the table above[/red]" ) except ValueError: console.print("[red]Please enter a valid number[/red]") From d2ac860f664162c9aed455bdf3d41017b27bf7dc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 31 Jan 2025 16:18:28 -0800 Subject: [PATCH 262/332] Removes bt-decode --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4b903f3d..e4003d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ scalecodec==1.2.11 typer~=0.12 websockets>=14.1 bittensor-wallet>=3.0.0 -bt-decode==v0.5.0-a0 plotille pywry plotly \ No newline at end of file From 1191f7aceb1e801cc7adac676755a440dd55b80b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 12:08:43 -0800 Subject: [PATCH 263/332] Updates e2e for senate --- tests/e2e_tests/test_senate.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index f335ada5..a0c53d75 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -44,7 +44,7 @@ def test_senate(local_chain, wallet_setup): # Fetch existing senate list root_senate = exec_command_bob( - command="root", + command="sudo", sub_command="senate", extra_args=[ "--network", @@ -58,9 +58,11 @@ def test_senate(local_chain, wallet_setup): # Register Bob to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_bob( - command="root", + command="subnets", sub_command="register", extra_args=[ + "--netuid", + "0", "--wallet-path", wallet_path_bob, "--network", @@ -76,7 +78,7 @@ def test_senate(local_chain, wallet_setup): # Fetch the senate members after registering to root root_senate_after_reg = exec_command_bob( - command="root", + command="sudo", sub_command="senate", extra_args=[ "--chain", @@ -93,7 +95,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after adding one proposals = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", @@ -117,7 +119,7 @@ def test_senate(local_chain, wallet_setup): # Vote on the proposal by Bob (vote aye) vote_aye = exec_command_bob( - command="root", + command="sudo", sub_command="senate-vote", extra_args=[ "--wallet-path", @@ -138,7 +140,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after voting aye proposals_after_aye = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", @@ -160,9 +162,11 @@ def test_senate(local_chain, wallet_setup): # Register Alice to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_alice( - command="root", + command="subnets", sub_command="register", extra_args=[ + "--netuid", + "0", "--wallet-path", wallet_path_alice, "--chain", @@ -178,7 +182,7 @@ def test_senate(local_chain, wallet_setup): # Vote on the proposal by Alice (vote nay) vote_nay = exec_command_alice( - command="root", + command="sudo", sub_command="senate-vote", extra_args=[ "--wallet-path", @@ -199,7 +203,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after voting proposals_after_nay = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", From 9406187af784ea6f952377676ca51984031ec21d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 23:46:02 +0200 Subject: [PATCH 264/332] Add exit_early arg for CLIManager._run_command so as to allow reuse of the Subtensor object in the handful of calls where it's necessary. --- bittensor_cli/cli.py | 43 ++++++++++--------- .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/commands/sudo.py | 7 +++ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f8e2ab49..1b85403a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -900,7 +900,7 @@ def initialize_chain( self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor - def _run_command(self, cmd: Coroutine) -> None: + def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ Runs the supplied coroutine with `asyncio.run` """ @@ -933,11 +933,12 @@ async def _run(): finally: if initiated is False: asyncio.create_task(cmd).cancel() - try: - raise typer.Exit() - except Exception as e: # ensures we always exit cleanly - err_console.print(f"An unknown error has occurred: {e}") - sys.exit() + if exit_early is True: + try: + raise typer.Exit() + except Exception as e: # ensures we always exit cleanly + err_console.print(f"An unknown error has occurred: {e}") + sys.exit() if sys.version_info < (3, 10): # For Python 3.9 or lower @@ -2389,7 +2390,8 @@ def wallet_set_id( self.initialize_chain(network), wallet.coldkeypub.ss58_address, "Current on-chain identity", - ) + ), + exit_early=False, ) if prompt: @@ -2692,7 +2694,8 @@ def stake_add( max_rows=12, prompt=False, delegate_selection=True, - ) + ), + exit_early=False, ) if selected_hotkey is None: print_error("No delegate selected. Exiting.") @@ -2752,7 +2755,8 @@ def stake_add( free_balance, staked_balance = self._run_command( wallets.wallet_balance( wallet, self.initialize_chain(network), False, None - ) + ), + exit_early=False, ) if free_balance == Balance.from_tao(0): print_error("You dont have any balance to stake.") @@ -3669,7 +3673,8 @@ def sudo_set( self.verbosity_handler(quiet, verbose) hyperparams = self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid) + sudo.get_hyperparameters(self.initialize_chain(network), netuid), + exit_early=False, ) if not hyperparams: @@ -3840,11 +3845,9 @@ def sudo_set_take( validate=WV.WALLET_AND_HOTKEY, ) - current_take = self._run_command( - sudo.get_current_take(self.initialize_chain(network), wallet) - ) - console.print( - f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet), + exit_early=False, ) if not take: @@ -3888,11 +3891,8 @@ def sudo_get_take( validate=WV.WALLET_AND_HOTKEY, ) - current_take = self._run_command( - sudo.get_current_take(self.initialize_chain(network), wallet) - ) - console.print( - f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet) ) def subnets_list( @@ -4123,7 +4123,8 @@ def subnets_create( subnet_contact=subnet_contact, ) success = self._run_command( - subnets.create(wallet, self.initialize_chain(network), identity, prompt) + subnets.create(wallet, self.initialize_chain(network), identity, prompt), + exit_early=False, ) if success and prompt: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index caca2446..a1b61662 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -172,7 +172,7 @@ async def get_all_subnet_netuids( ) res = [] async for netuid, exists in result: - if exists.value: # TODO verify type + if exists.value: res.append(netuid) return res diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index ec7a6f78..60a107a2 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -684,6 +684,13 @@ async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet): return current_take +async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) -> None: + current_take = await get_current_take(subtensor, wallet) + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) + + async def set_take( wallet: Wallet, subtensor: "SubtensorInterface", take: float ) -> bool: From 4902a8fe437fca761aa13d925c1317c7b5476b9b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 3 Feb 2025 23:53:01 +0200 Subject: [PATCH 265/332] Don't print error message for typer Exit --- bittensor_cli/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1b85403a..f00f3410 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -937,8 +937,9 @@ async def _run(): try: raise typer.Exit() except Exception as e: # ensures we always exit cleanly - err_console.print(f"An unknown error has occurred: {e}") - sys.exit() + if not isinstance(e, typer.Exit): + err_console.print(f"An unknown error has occurred: {e}") + sys.exit() if sys.version_info < (3, 10): # For Python 3.9 or lower From 3cf817eaad317d3984ac817b3363a77176706482 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 4 Feb 2025 00:04:17 +0200 Subject: [PATCH 266/332] Removed sys.exit --- bittensor_cli/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f00f3410..174af5d5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -939,7 +939,6 @@ async def _run(): except Exception as e: # ensures we always exit cleanly if not isinstance(e, typer.Exit): err_console.print(f"An unknown error has occurred: {e}") - sys.exit() if sys.version_info < (3, 10): # For Python 3.9 or lower From 337f416e93001546eeddf01768fec8e315ae732f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 14:09:46 -0800 Subject: [PATCH 267/332] Updates dynamicinfo + minor improvements --- CHANGELOG.md | 7 ++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 4 ++++ bittensor_cli/src/commands/subnets/subnets.py | 22 +++++++++++++------ 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de809acc..ef8621b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 8.2.0rc15 /2025-02-03 + +## What's Changed +* Adds subnet volume to root DynamicInfo +* Adds --no-prompt option for stake list +* Removes index and adds uids/position for delegate selection + ## 8.2.0rc14 /2025-01-24 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 8ecf1c97..6274e84c 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc14" +__version__ = "8.2.0rc15" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7393ee8e..194bc8fb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -64,7 +64,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc14" +__version__ = "8.2.0rc15" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 97949867..209cc0ca 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -900,6 +900,7 @@ class DynamicInfo: pending_root_emission: Balance network_registered_at: int subnet_identity: Optional[SubnetIdentity] + subnet_volume: float @classmethod def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: @@ -938,6 +939,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) + subnet_volume = Balance.from_rao(decoded["subnet_volume"]).set_unit(netuid) alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( netuid ) @@ -991,6 +993,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": pending_root_emission=pending_root_emission, network_registered_at=int(decoded["network_registered_at"]), subnet_identity=subnet_identity, + subnet_volume=subnet_volume, ) def tao_to_alpha(self, tao: Balance) -> Balance: @@ -1601,6 +1604,7 @@ def decode(result: list[int]) -> list[dict]: ["pending_alpha_emission", "Compact"], ["pending_root_emission", "Compact"], ["network_registered_at", "Compact"], + ["subnet_volume", "Compact"], ["subnet_identity", "Option"], ], }, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c271c430..041261be 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -995,15 +995,23 @@ async def show_root(): if selection == "": return None - - uid = int(selection) - selected_hotkey = root_state.hotkeys[uid] - # Find identity for this UID - coldkey_identity = identities.get(root_state.coldkeys[uid], {}).get("name", "") + + position = int(selection) + idx = position - 1 + original_idx = sorted_hotkeys[idx][0] + selected_hotkey = root_state.hotkeys[original_idx] + + coldkey_identity = identities.get( + root_state.coldkeys[original_idx], {} + ).get("name", "") hotkey_identity = old_identities.get(selected_hotkey) - validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") + validator_identity = ( + coldkey_identity + if coldkey_identity + else (hotkey_identity.display if hotkey_identity else "") + ) identity_str = f" ({validator_identity})" if validator_identity else "" - + console.print( f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" ) From 55225eb02f12326be2c563103f70e5980bf6c6e5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 14:11:29 -0800 Subject: [PATCH 268/332] Updates release script --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed1fc6d5..88ce34a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: fi - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: dist/ @@ -60,7 +60,7 @@ jobs: steps: - name: Download artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist path: dist/ From d921a96eda5916012b16a93889cbe2dfb79d5663 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 14:17:25 -0800 Subject: [PATCH 269/332] Updated return type --- bittensor_cli/src/bittensor/chain_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 209cc0ca..f846ae0b 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -900,7 +900,7 @@ class DynamicInfo: pending_root_emission: Balance network_registered_at: int subnet_identity: Optional[SubnetIdentity] - subnet_volume: float + subnet_volume: Balance @classmethod def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: From 5f0e22df6d8526ee32c2398513121a11eff41189 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 3 Feb 2025 20:26:40 -0800 Subject: [PATCH 270/332] staking e2e working --- bittensor_cli/src/commands/stake/stake.py | 96 +++++++++++------------ tests/e2e_tests/conftest.py | 2 +- tests/e2e_tests/test_root.py | 3 +- tests/e2e_tests/test_staking_sudo.py | 37 +++++---- 4 files changed, 68 insertions(+), 70 deletions(-) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index d1f6289c..959fc9eb 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -302,10 +302,6 @@ async def send_extrinsic( f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" ) return - if not prompt: # TODO verbose? - console.print( - f":white_heavy_check_mark: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Submitted {amount_} to {netuid_i}[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - ) else: await response.process_events() if not await response.is_success: @@ -327,7 +323,7 @@ async def send_extrinsic( ): new_stake = stake_info.stake.set_unit(netuid_i) break - + console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) @@ -1086,23 +1082,21 @@ async def unstake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) else: - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) else: for op in unstake_operations: netuid_i = op["netuid"] @@ -1130,38 +1124,36 @@ async def unstake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) else: - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_stake_info = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - ) - new_stake = Balance.from_rao(0) - for stake_info in new_stake_info: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_stake_info = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + new_stake = Balance.from_rao(0) + for stake_info in new_stake_info: + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): + new_stake = stake_info.stake.set_unit(netuid_i) + break + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 4d9f9c7b..aaf8dde8 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -18,7 +18,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = os.getenv("LOCALNET_SH_PATH") + script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" if not script_path: # Skip the test if the localhost.sh path is not set diff --git a/tests/e2e_tests/test_root.py b/tests/e2e_tests/test_root.py index 1e87dbfa..5a7674bb 100644 --- a/tests/e2e_tests/test_root.py +++ b/tests/e2e_tests/test_root.py @@ -1,6 +1,7 @@ import time from bittensor_cli.src.bittensor.balances import Balance +import pytest from .utils import extract_coldkey_balance """ @@ -15,7 +16,7 @@ * btcli root undelegate-stake """ - +@pytest.mark.skip(reason="Root no longer applicable. We will update this.") def test_root_commands(local_chain, wallet_setup): """ Test the root commands and inspects their output diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 16276486..abb3fe5d 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -32,7 +32,7 @@ def test_staking(local_chain, wallet_setup): AssertionError: If any of the checks or verifications fail """ print("Testing staking and sudo commands🧪") - netuid = 1 + netuid = 2 wallet_path_alice = "//Alice" # Create wallet for Alice @@ -47,11 +47,19 @@ def test_staking(local_chain, wallet_setup): extra_args=[ "--wallet-path", wallet_path_alice, + "--wallet-hotkey", + wallet_alice.hotkey_str, "--chain", "ws://127.0.0.1:9945", "--wallet-name", wallet_alice.name, "--no-prompt", + "--subnet-name", + "test-subnet", + "--github-repo", + "https://github.com/bittensor/bittensor", + "--subnet-contact", + "test@test.com", ], ) assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout @@ -74,13 +82,15 @@ def test_staking(local_chain, wallet_setup): "--no-prompt", ], ) - assert "✅ Registered" in register_subnet.stdout + assert "✅ Already Registered" in register_subnet.stdout # Add stake to Alice's hotkey add_stake = exec_command_alice( command="stake", sub_command="add", extra_args=[ + "--netuid", + netuid, "--wallet-path", wallet_path_alice, "--wallet-name", @@ -99,7 +109,7 @@ def test_staking(local_chain, wallet_setup): # Execute stake show for Alice's wallet show_stake = exec_command_alice( command="stake", - sub_command="show", + sub_command="list", extra_args=[ "--wallet-path", wallet_path_alice, @@ -113,19 +123,16 @@ def test_staking(local_chain, wallet_setup): cleaned_stake = [ re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines() ] - stake_added = cleaned_stake[6].split()[6].strip("τ") - assert Balance.from_tao(100) == Balance.from_tao(float(stake_added)) - - # TODO: Ask nucleus the rate limit and wait epoch - # Sleep 120 seconds for rate limiting when unstaking - print("Waiting for interval for 2 minutes") - time.sleep(120) + stake_added = cleaned_stake[9].split()[8] + assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(100) # Execute remove_stake command and remove all 100 TAO from Alice remove_stake = exec_command_alice( command="stake", sub_command="remove", extra_args=[ + "--netuid", + netuid, "--wallet-path", wallet_path_alice, "--wallet-name", @@ -155,10 +162,10 @@ def test_staking(local_chain, wallet_setup): # Parse all hyperparameters and single out max_burn in TAO all_hyperparams = hyperparams.stdout.splitlines() - max_burn_tao = all_hyperparams[22].split()[2] + max_burn_tao = all_hyperparams[22].split()[3] # Assert max_burn is 100 TAO from default - assert Balance.from_tao(float(max_burn_tao.strip("τ"))) == Balance.from_tao(100) + assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100) # Change max_burn hyperparameter to 10 TAO change_hyperparams = exec_command_alice( @@ -199,10 +206,8 @@ def test_staking(local_chain, wallet_setup): # Parse updated hyperparameters all_updated_hyperparams = updated_hyperparams.stdout.splitlines() - updated_max_burn_tao = all_updated_hyperparams[22].split()[2] + updated_max_burn_tao = all_updated_hyperparams[22].split()[3] # Assert max_burn is now 10 TAO - assert Balance.from_tao(float(updated_max_burn_tao.strip("τ"))) == Balance.from_tao( - 10 - ) + assert Balance.from_tao(float(updated_max_burn_tao)) == Balance.from_tao(10) print("✅ Passed staking and sudo commands") From 63a3ed51d47a1ade4e9f905cee672341da06220a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 13:13:57 -0800 Subject: [PATCH 271/332] Updates slippage calculation --- bittensor_cli/src/commands/stake/move.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index d6ad378c..68211a5f 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -60,17 +60,25 @@ async def display_stake_movement_cross_subnets( price = ( float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) ) - received_amount_tao, _, slippage_pct_float = ( - dynamic_origin.alpha_to_tao_with_slippage(amount_to_move) + received_amount_tao, _, _ = dynamic_origin.alpha_to_tao_with_slippage( + amount_to_move ) received_amount_tao -= MIN_STAKE_FEE - received_amount, _, slippage_pct_float = ( - dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) + received_amount, _, _ = dynamic_destination.tao_to_alpha_with_slippage( + received_amount_tao ) received_amount.set_unit(destination_netuid) + + if received_amount < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + raise typer.Exit() + + ideal_amount = amount_to_move * price + total_slippage = ideal_amount - received_amount + slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) slippage_pct = f"{slippage_pct_float:.4f} %" price_str = ( - str(float(price)) + f"{price:.5f}" + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" ) From 6df502eae3070d98760de18bd5ffb82220dc43f8 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 13:17:37 -0800 Subject: [PATCH 272/332] Bumps version and changelog --- CHANGELOG.md | 13 +++++++++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de809acc..7992d747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 9.0.0rc1 /2025-02-04 + +## What's Changed +* Uses new Async Substrate Interface +* Updates how we use runtime calls + +## 8.2.0rc15 /2025-02-03 + +## What's Changed +* Adds subnet volume to root DynamicInfo +* Adds --no-prompt option for stake list +* Removes index and adds uids/position for delegate selection + ## 8.2.0rc14 /2025-01-24 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 3f62132b..32f17677 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0" +__version__ = "9.0.0rc1" __all__ = [CLIManager, __version__] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 174af5d5..9c15685f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -65,7 +65,7 @@ class GitError(Exception): pass -__version__ = "8.2.0rc14" +__version__ = "9.0.0rc1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 696c68697f9b3ee0b96ce6e4c295b268644f3fd5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 14:38:42 -0800 Subject: [PATCH 273/332] Updates commit_reveal_enabled hyperparam name --- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a3379ef0..6d3b805c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -646,7 +646,7 @@ class WalletValidationTypes(Enum): "kappa": "sudo_set_kappa", "difficulty": "sudo_set_difficulty", "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", + "commit_reveal_period": "sudo_set_commit_reveal_weights_interval", "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", "alpha_values": "sudo_set_alpha_values", "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 20e1f7a1..ef8c59a2 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -146,7 +146,7 @@ class SubnetHyperparameters(InfoBase): max_validators: int adjustment_alpha: int difficulty: int - commit_reveal_weights_interval: int + commit_reveal_period: int commit_reveal_weights_enabled: bool alpha_high: int alpha_low: int @@ -179,7 +179,7 @@ def _fix_decoded( max_validators=decoded.get("max_validators"), adjustment_alpha=decoded.get("adjustment_alpha"), difficulty=decoded.get("difficulty"), - commit_reveal_weights_interval=decoded.get( + commit_reveal_period=decoded.get( "commit_reveal_weights_interval" ), commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), From 49e63508b9a789d08d46eb4e75a8d559f81a882e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 5 Feb 2025 00:44:55 +0200 Subject: [PATCH 274/332] Use `query_multi` rather than `query_multiple` --- bittensor_cli/cli.py | 4 +-- .../src/bittensor/subtensor_interface.py | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9c15685f..58ada4a8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -53,7 +53,7 @@ ) from typing_extensions import Annotated from textwrap import dedent -from websockets import ConnectionClosed +from websockets import ConnectionClosed, InvalidHandshake from yaml import safe_dump, safe_load try: @@ -916,7 +916,7 @@ async def _run(): initiated = True result = await cmd return result - except (ConnectionRefusedError, ssl.SSLError): + except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake): err_console.print(f"Unable to connect to the chain: {self.subtensor}") verbose_console.print(traceback.format_exc()) except ( diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a1b61662..5b0d6c9a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -426,14 +426,26 @@ async def get_total_stake_for_hotkey( ... } """ + if not block_hash: + if reuse_block: + block_hash = self.substrate.last_block_hash + else: + block_hash = await self.substrate.get_chain_head() + netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash) - query: dict[tuple[str, int], int] = await self.substrate.query_multiple( - params=[(ss58, netuid) for ss58 in ss58_addresses for netuid in netuids], - module="SubtensorModule", - storage_function="TotalHotkeyAlpha", - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + calls = [ + ( + await self.substrate.create_storage_key( + "SubtensorModule", + "TotalHotkeyAlpha", + params=[ss58, netuid], + block_hash=block_hash, + ) + ) + for ss58 in ss58_addresses + for netuid in netuids + ] + query = await self.substrate.query_multi(calls, block_hash=block_hash) results: dict[str, dict[int, "Balance"]] = { hk_ss58: {} for hk_ss58 in ss58_addresses } From c5d0c76ba9ff243a393454e69d8f1403718dbef7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 14:53:31 -0800 Subject: [PATCH 275/332] Updates chain data --- bittensor_cli/src/bittensor/chain_data.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 4874acc5..859a2cd6 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -637,14 +637,17 @@ def _fix_decoded(cls, decoded: Any) -> "DynamicInfo": owner_hotkey = decode_account_id(decoded.get("owner_hotkey")) owner_coldkey = decode_account_id(decoded.get("owner_coldkey")) - emission = Balance.from_rao(decoded["emission"]).set_unit(0) - alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) - alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) - tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) - subnet_volume = Balance.from_rao(decoded["subnet_volume"]).set_unit(netuid) - alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( + emission = Balance.from_rao(decoded.get("emission")).set_unit(0) + alpha_in = Balance.from_rao(decoded.get("alpha_in")).set_unit(netuid) + alpha_out = Balance.from_rao(decoded.get("alpha_out")).set_unit(netuid) + tao_in = Balance.from_rao(decoded.get("tao_in")).set_unit(0) + alpha_out_emission = Balance.from_rao( + decoded.get("alpha_out_emission") + ).set_unit(netuid) + alpha_in_emission = Balance.from_rao(decoded.get("alpha_in_emission")).set_unit( netuid ) + subnet_volume = Balance.from_rao(decoded.get("subnet_volume")).set_unit(netuid) tao_in_emission = Balance.from_rao(decoded.get("tao_in_emission")).set_unit(0) pending_alpha_emission = Balance.from_rao( decoded.get("pending_alpha_emission") From adf9b8833bdf36654d2a687c9c53e6e7057d4d40 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 15:28:00 -0800 Subject: [PATCH 276/332] Adds ss58 option for origin hotkey for stake move --- bittensor_cli/cli.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fb599cdf..5038ca06 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2580,7 +2580,12 @@ def stake_list( return self._run_command( stake.stake_list( - wallet, coldkey_ss58, self.initialize_chain(network), live, verbose, no_prompt + wallet, + coldkey_ss58, + self.initialize_chain(network), + live, + verbose, + no_prompt, ) ) @@ -3123,22 +3128,25 @@ def stake_move( ) origin_hotkey = wallet.hotkey.ss58_address else: - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[], - validate=WV.WALLET_AND_HOTKEY, - ) - origin_hotkey = wallet.hotkey.ss58_address + if is_valid_ss58_address(wallet_hotkey): + origin_hotkey = wallet_hotkey + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[], + validate=WV.WALLET_AND_HOTKEY, + ) + origin_hotkey = wallet.hotkey.ss58_address if not interactive_selection: - if not origin_netuid: + if origin_netuid is None: origin_netuid = IntPrompt.ask( "Enter the [blue]origin subnet[/blue] (netuid) to move stake from" ) - if not destination_netuid: + if destination_netuid is None: destination_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) From 222a3937c5e78b23f606c996817b7038f36ed507 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 16:20:33 -0800 Subject: [PATCH 277/332] Updates btcli w overview --- bittensor_cli/src/commands/wallets.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 81fd75f6..5d9217d5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1052,31 +1052,13 @@ async def _fetch_all_neurons( ) -def _process_neurons_for_netuids( - netuids_with_all_neurons_hex_bytes: list[tuple[int, list[NeuronInfoLite]]], -) -> list[tuple[int, list[NeuronInfoLite]]]: - """ - Using multiprocessing to decode a list of hex-bytes neurons with their respective netuid - - :param netuids_with_all_neurons_hex_bytes: netuids with hex-bytes neurons - :return: netuids mapped to decoded neurons - """ - all_results = [ - (netuid, NeuronInfoLite.list_from_any(bytes.fromhex(result[2:]))) - for netuid, result in netuids_with_all_neurons_hex_bytes - ] - return all_results - - async def _get_neurons_for_netuids( subtensor: SubtensorInterface, netuids: list[int], hot_wallets: list[str] ) -> list[tuple[int, list["NeuronInfoLite"], Optional[str]]]: - all_neurons_hex_bytes = await _fetch_all_neurons(netuids, subtensor) - - all_processed_neurons = _process_neurons_for_netuids(all_neurons_hex_bytes) + all_neurons = await _fetch_all_neurons(netuids, subtensor) return [ _map_hotkey_to_neurons(neurons, hot_wallets, netuid) - for netuid, neurons in all_processed_neurons + for netuid, neurons in all_neurons ] From 3c681dac6a1ea2046d106044f709899c71094c99 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 20:40:01 -0800 Subject: [PATCH 278/332] Updates changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7992d747..0e540255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ## What's Changed * Uses new Async Substrate Interface * Updates how we use runtime calls +* Updates stake move to accept ss58 origin hotkeys +* Fixes slippage calculation in stake move +* Adds improved error handling through '--verbose' flag +* Improved docstrings ## 8.2.0rc15 /2025-02-03 From b6bcacbb55f1f20a749a289f44e65021ff558e09 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 21:25:05 -0800 Subject: [PATCH 279/332] Improves stake add --- bittensor_cli/cli.py | 10 +- bittensor_cli/src/commands/stake/stake.py | 245 +++++++++++++--------- 2 files changed, 149 insertions(+), 106 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 074ad874..9dc0188e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2601,12 +2601,6 @@ def stake_add( amount: float = typer.Option( 0.0, "--amount", help="The amount of TAO to stake" ), - max_stake: float = typer.Option( - 0.0, - "--max-stake", - "-m", - help="Stake is sent to a hotkey only until the hotkey's total stake is less than or equal to this maximum staked TAO. If a hotkey already has stake greater than this amount, then stake is not added to this hotkey.", - ), include_hotkeys: str = typer.Option( "", "--include-hotkeys", @@ -2757,7 +2751,7 @@ def stake_add( excluded_hotkeys = [] # TODO: Ask amount for each subnet explicitly if more than one - if not stake_all and not amount and not max_stake: + if not stake_all and not amount: free_balance, staked_balance = self._run_command( wallets.wallet_balance( wallet, self.initialize_chain(network), False, None @@ -2792,9 +2786,7 @@ def stake_add( netuid, stake_all, amount, - False, prompt, - max_stake, all_hotkeys, included_hotkeys, excluded_hotkeys, diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 2ea5dd8e..29684c1d 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -35,15 +35,149 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +def _get_hotkeys_to_stake_to( + wallet: Wallet, + all_hotkeys: bool = False, + include_hotkeys: list[str] = None, + exclude_hotkeys: list[str] = None, +) -> list[tuple[Optional[str], str]]: + """Get list of hotkeys to stake to based on input parameters. + + Args: + wallet: The wallet containing hotkeys + all_hotkeys: If True, get all hotkeys from wallet except excluded ones + include_hotkeys: List of specific hotkeys to include (by name or ss58 address) + exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True + + Returns: + List of tuples containing (hotkey_name, hotkey_ss58_address) + hotkey_name may be None if ss58 address was provided directly + """ + if all_hotkeys: + # Stake to all hotkeys except excluded ones + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + return [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in (exclude_hotkeys or []) + ] + + if include_hotkeys: + print_verbose("Staking to only included hotkeys") + # Stake to specific hotkeys + hotkeys = [] + for hotkey_ss58_or_hotkey_name in include_hotkeys: + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + # If valid ss58 address, add directly + hotkeys.append((None, hotkey_ss58_or_hotkey_name)) + else: + # If hotkey name, get ss58 from wallet + wallet_ = Wallet( + path=wallet.path, + name=wallet.name, + hotkey=hotkey_ss58_or_hotkey_name, + ) + hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) + return hotkeys + + # Default: stake to single hotkey from wallet + print_verbose( + f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" + ) + assert wallet.hotkey is not None + return [(None, wallet.hotkey.ss58_address)] + + +def _define_stake_add_table(wallet: Wallet, subtensor: "SubtensorInterface") -> Table: + """Creates and initializes a table for displaying stake information. + + Args: + wallet: The wallet being used for staking + subtensor: The subtensor interface + + Returns: + Table: An initialized rich Table object with appropriate columns + """ + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate (per {Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Received", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + + return table + + +def _print_table_and_slippage(table: Table, max_slippage: float): + """Prints the stake table, slippage warning, and table description. + + Args: + table: The rich Table object to print + max_slippage: The maximum slippage percentage across all operations + """ + console.print(table) + + # Greater than 5% + if max_slippage > 5: + message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + # Table description + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. + - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. + - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + + async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int], stake_all: bool, amount: float, - delegate: bool, prompt: bool, - max_stake: float, all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], @@ -71,17 +205,11 @@ async def stake_add( if netuid is not None else await subtensor.get_all_subnet_netuids() ) - # Init the table. - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, + hotkeys_to_stake_to = _get_hotkeys_to_stake_to( + wallet=wallet, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, ) # Determine the amount we are staking. @@ -95,46 +223,6 @@ async def stake_add( remaining_wallet_balance = current_wallet_balance max_slippage = 0.0 - hotkeys_to_stake_to: list[tuple[Optional[str], str]] = [] - if all_hotkeys: - # Stake to all hotkeys. - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - # Get the hotkeys to exclude. (d)efault to no exclusions. - # Exclude hotkeys that are specified. - hotkeys_to_stake_to = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in exclude_hotkeys - ] # definitely wallets - - elif include_hotkeys: - print_verbose("Staking to only included hotkeys") - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - path=wallet.path, - name=wallet.name, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys_to_stake_to.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - else: - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - print_verbose( - f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" - ) - assert wallet.hotkey is not None - hotkey_ss58_or_name = wallet.hotkey.ss58_address - hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] - starting_chain_head = await subtensor.substrate.get_chain_head() _all_dynamic_info, stake_info_dict = await asyncio.gather( subtensor.all_subnets(), @@ -145,7 +233,7 @@ async def stake_add( ) all_dynamic_info = {di.netuid: di for di in _all_dynamic_info} initial_stake_balances = {} - for hotkey_ss58 in [x[1] for x in hotkeys_to_stake_to]: + for _, hotkey_ss58 in hotkeys_to_stake_to: initial_stake_balances[hotkey_ss58] = {} for netuid in netuids: initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) @@ -177,7 +265,7 @@ async def stake_add( amount_to_stake_as_balance = Balance.from_tao(amount) elif stake_all: amount_to_stake_as_balance = current_wallet_balance / len(netuids) - elif not amount and not max_stake: + elif not amount: if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): amount_to_stake_as_balance = remaining_wallet_balance else: @@ -226,50 +314,13 @@ async def stake_add( str(slippage_pct), ) ) - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Amount ({Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO"], - ) - table.add_column( - f"Rate (per {Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - "Received", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) + + # Define and print stake table + slippage warning + table = _define_stake_add_table(wallet, subtensor) for row in rows: table.add_row(*row) - console.print(table) - message = "" - if max_slippage > 5: - message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - console.print( - """ -[bold white]Description[/bold white]: -The table displays information about the stake operation you are about to perform. -The columns are as follows: - - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. - - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. - - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). -""" - ) + _print_table_and_slippage(table, max_slippage) + if prompt: if not Confirm.ask("Would you like to continue?"): raise typer.Exit() From cf7e7b3098e2136b78f07bbb6ba2fa3e0b14eb4f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 4 Feb 2025 23:09:32 -0800 Subject: [PATCH 280/332] Overhauled stake add --- bittensor_cli/cli.py | 9 +- bittensor_cli/src/commands/stake/add.py | 410 ++++++++++++++++++++++ bittensor_cli/src/commands/stake/move.py | 2 +- bittensor_cli/src/commands/stake/stake.py | 387 +------------------- 4 files changed, 419 insertions(+), 389 deletions(-) create mode 100644 bittensor_cli/src/commands/stake/add.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9dc0188e..1c0bc65c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,12 @@ from bittensor_cli.src.commands import sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets -from bittensor_cli.src.commands.stake import children_hotkeys, stake, move +from bittensor_cli.src.commands.stake import ( + children_hotkeys, + stake, + move, + add as stake_add, +) from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters from bittensor_cli.src.bittensor.utils import ( @@ -2780,7 +2785,7 @@ def stake_add( raise typer.Exit() return self._run_command( - stake.stake_add( + stake_add.stake_add( wallet, self.initialize_chain(network), netuid, diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py new file mode 100644 index 00000000..b22e4f8d --- /dev/null +++ b/bittensor_cli/src/commands/stake/add.py @@ -0,0 +1,410 @@ +import asyncio +from functools import partial + +import typer +from typing import TYPE_CHECKING, Optional +from rich.table import Table +from rich.prompt import Confirm + +from async_substrate_interface.errors import SubstrateRequestException +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + format_error_message, + get_hotkey_wallets_for_wallet, + is_valid_ss58_address, + print_error, + print_verbose, + prompt_stake_amount, +) +from bittensor_wallet import Wallet +from bittensor_wallet.errors import KeyFileError + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +def _get_hotkeys_to_stake_to( + wallet: Wallet, + all_hotkeys: bool = False, + include_hotkeys: list[str] = None, + exclude_hotkeys: list[str] = None, +) -> list[tuple[Optional[str], str]]: + """Get list of hotkeys to stake to based on input parameters. + + Args: + wallet: The wallet containing hotkeys + all_hotkeys: If True, get all hotkeys from wallet except excluded ones + include_hotkeys: List of specific hotkeys to include (by name or ss58 address) + exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True + + Returns: + List of tuples containing (hotkey_name, hotkey_ss58_address) + hotkey_name may be None if ss58 address was provided directly + """ + if all_hotkeys: + # Stake to all hotkeys except excluded ones + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + return [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in (exclude_hotkeys or []) + ] + + if include_hotkeys: + print_verbose("Staking to only included hotkeys") + # Stake to specific hotkeys + hotkeys = [] + for hotkey_ss58_or_hotkey_name in include_hotkeys: + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + # If valid ss58 address, add directly + hotkeys.append((None, hotkey_ss58_or_hotkey_name)) + else: + # If hotkey name, get ss58 from wallet + wallet_ = Wallet( + path=wallet.path, + name=wallet.name, + hotkey=hotkey_ss58_or_hotkey_name, + ) + hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) + + return hotkeys + + # Default: stake to single hotkey from wallet + print_verbose( + f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" + ) + assert wallet.hotkey is not None + return [(None, wallet.hotkey.ss58_address)] + + +def define_stake_stable(wallet: Wallet, subtensor: "SubtensorInterface") -> Table: + """Creates and initializes a table for displaying stake information. + + Args: + wallet: The wallet being used for staking + subtensor: The subtensor interface + + Returns: + Table: An initialized rich Table object with appropriate columns + """ + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate (per {Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Received", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + + return table + + +def print_table_and_slippage(table: Table, max_slippage: float): + """Prints the stake table, slippage warning, and table description. + + Args: + table: The rich Table object to print + max_slippage: The maximum slippage percentage across all operations + """ + console.print(table) + + # Greater than 5% + if max_slippage > 5: + message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + # Table description + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. + - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. + - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + + +def calculate_slippage( + subnet_info, amount: Balance +) -> tuple[Balance, str, float]: + """Calculate slippage when adding stake. + + Args: + subnet_info: Subnet dynamic info + amount: Amount being staked + + Returns: + tuple containing: + - received_amount: Amount received after slippage + - slippage_str: Formatted slippage percentage string + - slippage_float: Raw slippage percentage value + """ + received_amount, _, slippage_pct_float = subnet_info.tao_to_alpha_with_slippage( + amount + ) + if subnet_info.is_dynamic: + slippage_str = f"{slippage_pct_float:.4f} %" + rate = str(1 / (float(subnet_info.price) or 1)) + else: + slippage_pct_float = 0 + slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" + rate = str(1) + + return received_amount, slippage_str, slippage_pct_float, rate + + +async def stake_add( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: Optional[int], + stake_all: bool, + amount: float, + prompt: bool, + all_hotkeys: bool, + include_hotkeys: list[str], + exclude_hotkeys: list[str], +): + """ + + Args: + wallet: wallet object + subtensor: SubtensorInterface object + netuid: the netuid to stake to (None indicates all subnets) + stake_all: whether to stake all available balance + amount: specified amount of balance to stake + delegate: whether to delegate stake, currently unused + prompt: whether to prompt the user + max_stake: maximum amount to stake (used in combination with stake_all), currently unused + all_hotkeys: whether to stake all hotkeys + include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) + exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) + + Returns: + bool: True if stake operation is successful, False otherwise + """ + + async def send_stake_extrinsic( + netuid_i, amount_, current, staking_address_ss58, status=None + ): + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" + ) + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.coldkeypub.ss58_address + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_staked": amount_.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce + ) + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + except SubstrateRequestException as e: + err_out( + f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + ) + return + else: + await response.process_events() + if not await response.is_success: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + ) + else: + new_balance, stake_info_dict = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + ), + ) + new_stake = Balance.from_rao(0) + for stake_info in stake_info_dict: + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): + new_stake = stake_info.stake.set_unit(netuid_i) + break + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + + netuids = ( + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() + ) + + hotkeys_to_stake_to = _get_hotkeys_to_stake_to( + wallet=wallet, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, + ) + + # Get subnet data and stake information for coldkey + chain_head = await subtensor.substrate.get_chain_head() + _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather( + subtensor.all_subnets(), + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + block_hash=chain_head, + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address), + ) + all_subnets = {di.netuid: di for di in _all_subnets} + + # Map current stake balances for hotkeys + hotkey_stake_map = {} + for _, hotkey_ss58 in hotkeys_to_stake_to: + hotkey_stake_map[hotkey_ss58] = {} + for netuid in netuids: + hotkey_stake_map[hotkey_ss58][netuid] = Balance.from_rao(0) + + for stake_info in _stake_info: + if stake_info.hotkey_ss58 in hotkey_stake_map: + hotkey_stake_map[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) + + # Determine the amount we are staking. + rows = [] + amounts_to_stake = [] + current_stake_balances = [] + remaining_wallet_balance = current_wallet_balance + max_slippage = 0.0 + + for hotkey in hotkeys_to_stake_to: + for netuid in netuids: + # Check that the subnet exists. + dynamic_info = all_subnets.get(netuid) + if not dynamic_info: + err_console.print(f"Subnet with netuid: {netuid} does not exist.") + continue + current_stake_balances.append(hotkey_stake_map[hotkey[1]][netuid]) + + # Get the amount. + amount_to_stake = Balance(0) + if amount: + amount_to_stake = Balance.from_tao(amount) + elif stake_all: + amount_to_stake = current_wallet_balance / len(netuids) + elif not amount: + amount_to_stake, _ = prompt_stake_amount( + current_balance=remaining_wallet_balance, + netuid=netuid, + action_name="stake", + ) + amounts_to_stake.append(amount_to_stake) + + # Check enough to stake. + if amount_to_stake > remaining_wallet_balance: + err_console.print( + f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " + f"staking amount: {amount_to_stake}[/bold white]" + ) + return False + remaining_wallet_balance -= amount_to_stake + + # Calculate slippage + received_amount, slippage_pct, slippage_pct_float, rate = ( + calculate_slippage(dynamic_info, amount_to_stake) + ) + max_slippage = max(slippage_pct_float, max_slippage) + + # Add rows for the table + rows.append( + ( + str(netuid), + f"{hotkey[1]}", + str(amount_to_stake), + rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + str(received_amount.set_unit(netuid)), + str(slippage_pct), + ) + ) + + # Define and print stake table + slippage warning + table = define_stake_stable(wallet, subtensor) + for row in rows: + table.add_row(*row) + print_table_and_slippage(table, max_slippage) + + if prompt: + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Perform staking operation. + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + stake_coroutines = [ + send_stake_extrinsic(ni, am, curr, staking_address) + for i, (ni, am, curr) in enumerate( + zip(netuids, amounts_to_stake, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + ] + await asyncio.gather(*stake_coroutines) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 68211a5f..64be393f 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -178,7 +178,7 @@ def prompt_stake_amount( """ while True: amount_input = Prompt.ask( - f"\nEnter amount to {action_name} from " + f"\nEnter the amount to {action_name}" f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " f"or " diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 29684c1d..68aef724 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1,18 +1,16 @@ import asyncio -from functools import partial from typing import TYPE_CHECKING, Optional import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm, FloatPrompt, Prompt +from rich.prompt import Confirm, Prompt from rich.table import Table from rich import box from rich.progress import Progress, BarColumn, TextColumn from rich.console import Group from rich.live import Live -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance @@ -35,389 +33,6 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -def _get_hotkeys_to_stake_to( - wallet: Wallet, - all_hotkeys: bool = False, - include_hotkeys: list[str] = None, - exclude_hotkeys: list[str] = None, -) -> list[tuple[Optional[str], str]]: - """Get list of hotkeys to stake to based on input parameters. - - Args: - wallet: The wallet containing hotkeys - all_hotkeys: If True, get all hotkeys from wallet except excluded ones - include_hotkeys: List of specific hotkeys to include (by name or ss58 address) - exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True - - Returns: - List of tuples containing (hotkey_name, hotkey_ss58_address) - hotkey_name may be None if ss58 address was provided directly - """ - if all_hotkeys: - # Stake to all hotkeys except excluded ones - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - return [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in (exclude_hotkeys or []) - ] - - if include_hotkeys: - print_verbose("Staking to only included hotkeys") - # Stake to specific hotkeys - hotkeys = [] - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If valid ss58 address, add directly - hotkeys.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If hotkey name, get ss58 from wallet - wallet_ = Wallet( - path=wallet.path, - name=wallet.name, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) - return hotkeys - - # Default: stake to single hotkey from wallet - print_verbose( - f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" - ) - assert wallet.hotkey is not None - return [(None, wallet.hotkey.ss58_address)] - - -def _define_stake_add_table(wallet: Wallet, subtensor: "SubtensorInterface") -> Table: - """Creates and initializes a table for displaying stake information. - - Args: - wallet: The wallet being used for staking - subtensor: The subtensor interface - - Returns: - Table: An initialized rich Table object with appropriate columns - """ - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Amount ({Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO"], - ) - table.add_column( - f"Rate (per {Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - "Received", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) - - return table - - -def _print_table_and_slippage(table: Table, max_slippage: float): - """Prints the stake table, slippage warning, and table description. - - Args: - table: The rich Table object to print - max_slippage: The maximum slippage percentage across all operations - """ - console.print(table) - - # Greater than 5% - if max_slippage > 5: - message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - - # Table description - console.print( - """ -[bold white]Description[/bold white]: -The table displays information about the stake operation you are about to perform. -The columns are as follows: - - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. - - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. - - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). -""" - ) - - -async def stake_add( - wallet: Wallet, - subtensor: "SubtensorInterface", - netuid: Optional[int], - stake_all: bool, - amount: float, - prompt: bool, - all_hotkeys: bool, - include_hotkeys: list[str], - exclude_hotkeys: list[str], -): - """ - - Args: - wallet: wallet object - subtensor: SubtensorInterface object - netuid: the netuid to stake to (None indicates all subnets) - stake_all: whether to stake all available balance - amount: specified amount of balance to stake - delegate: whether to delegate stake, currently unused - prompt: whether to prompt the user - max_stake: maximum amount to stake (used in combination with stake_all), currently unused - all_hotkeys: whether to stake all hotkeys - include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) - exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) - - Returns: - - """ - netuids = ( - [int(netuid)] - if netuid is not None - else await subtensor.get_all_subnet_netuids() - ) - hotkeys_to_stake_to = _get_hotkeys_to_stake_to( - wallet=wallet, - all_hotkeys=all_hotkeys, - include_hotkeys=include_hotkeys, - exclude_hotkeys=exclude_hotkeys, - ) - - # Determine the amount we are staking. - rows = [] - stake_amount_balance = [] - current_stake_balances = [] - current_wallet_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - current_wallet_balance = current_wallet_balance_.set_unit(0) - remaining_wallet_balance = current_wallet_balance - max_slippage = 0.0 - - starting_chain_head = await subtensor.substrate.get_chain_head() - _all_dynamic_info, stake_info_dict = await asyncio.gather( - subtensor.all_subnets(), - subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=starting_chain_head, - ), - ) - all_dynamic_info = {di.netuid: di for di in _all_dynamic_info} - initial_stake_balances = {} - for _, hotkey_ss58 in hotkeys_to_stake_to: - initial_stake_balances[hotkey_ss58] = {} - for netuid in netuids: - initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) - - for stake_info in stake_info_dict: - if stake_info.hotkey_ss58 in initial_stake_balances: - initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = ( - stake_info.stake - ) - - for hk_name, hk_ss58 in hotkeys_to_stake_to: - if not is_valid_ss58_address(hk_ss58): - print_error( - f"The entered hotkey ss58 address is incorrect: {hk_name} | {hk_ss58}" - ) - return False - for hotkey in hotkeys_to_stake_to: - for netuid in netuids: - # Check that the subnet exists. - dynamic_info = all_dynamic_info.get(netuid) - if not dynamic_info: - err_console.print(f"Subnet with netuid: {netuid} does not exist.") - continue - current_stake_balances.append(initial_stake_balances[hotkey[1]][netuid]) - - # Get the amount. - amount_to_stake_as_balance = Balance(0) - if amount: - amount_to_stake_as_balance = Balance.from_tao(amount) - elif stake_all: - amount_to_stake_as_balance = current_wallet_balance / len(netuids) - elif not amount: - if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): - amount_to_stake_as_balance = remaining_wallet_balance - else: - try: - amount = FloatPrompt.ask( - f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}" - ) - amount_to_stake_as_balance = Balance.from_tao(amount) - except ValueError: - err_console.print( - f":cross_mark:[red]Invalid amount: {amount}[/red]" - ) - return False - stake_amount_balance.append(amount_to_stake_as_balance) - - # Check enough to stake. - amount_to_stake_as_balance.set_unit(0) - if amount_to_stake_as_balance > remaining_wallet_balance: - err_console.print( - f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " - f"staking amount: {amount_to_stake_as_balance}[/bold white]" - ) - return False - remaining_wallet_balance -= amount_to_stake_as_balance - - # Slippage warning - received_amount, _, slippage_pct_float = ( - dynamic_info.tao_to_alpha_with_slippage(amount_to_stake_as_balance) - ) - if dynamic_info.is_dynamic: - slippage_pct = f"{slippage_pct_float:.4f} %" - rate = str(1 / (float(dynamic_info.price) or 1)) - else: - slippage_pct_float = 0 - slippage_pct = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" - rate = str(1) - max_slippage = max(slippage_pct_float, max_slippage) - rows.append( - ( - str(netuid), - # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", - f"{hotkey[1]}", - str(amount_to_stake_as_balance), - rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - str(received_amount.set_unit(netuid)), - str(slippage_pct), - ) - ) - - # Define and print stake table + slippage warning - table = _define_stake_add_table(wallet, subtensor) - for row in rows: - table.add_row(*row) - _print_table_and_slippage(table, max_slippage) - - if prompt: - if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() - - async def send_extrinsic( - netuid_i, amount_, current, staking_address_ss58, status=None - ): - err_out = partial(print_error, status=status) - failure_prelude = ( - f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" - ) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_staked": amount_.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - except SubstrateRequestException as e: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" - ) - return - else: - await response.process_events() - if not await response.is_success: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" - ) - else: - new_balance, stake_info_dict = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - ), - ) - new_stake = Balance.from_rao(0) - for stake_info in stake_info_dict: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - - # Perform staking operation. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - extrinsics_coroutines = [ - send_extrinsic(ni, am, curr, staking_address) - for i, (ni, am, curr) in enumerate( - zip(netuids, stake_amount_balance, current_stake_balances) - ) - for _, staking_address in hotkeys_to_stake_to - ] - if len(extrinsics_coroutines) == 1: - with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): - await extrinsics_coroutines[0] - else: - with console.status(":satellite: Checking transaction rate limit ..."): - tx_rate_limit_blocks = await subtensor.query( - module="SubtensorModule", storage_function="TxRateLimit" - ) - netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] - for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs): - ni, hk = kp - with console.status( - f"\n:satellite: Staking on netuid {ni} with hotkey {hk}... ..." - ): - await item - if tx_rate_limit_blocks > 0: - with console.status( - f":hourglass: [yellow]Waiting for tx rate limit:" - f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ): - await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block - - async def unstake_selection( subtensor: "SubtensorInterface", wallet: Wallet, From 368e06309ee5e270e799fcc5be40b237d5e7716c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 09:50:04 -0800 Subject: [PATCH 281/332] naming update + row hints --- bittensor_cli/src/commands/stake/add.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index b22e4f8d..bc3dae98 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -222,7 +222,7 @@ async def stake_add( bool: True if stake operation is successful, False otherwise """ - async def send_stake_extrinsic( + async def add_stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None ): err_out = partial(print_error, status=status) @@ -374,12 +374,12 @@ async def send_stake_extrinsic( # Add rows for the table rows.append( ( - str(netuid), - f"{hotkey[1]}", - str(amount_to_stake), - rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - str(received_amount.set_unit(netuid)), - str(slippage_pct), + str(netuid), # netuid + f"{hotkey[1]}", # hotkey + str(amount_to_stake), # amount + rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate + str(received_amount.set_unit(netuid)), # received + str(slippage_pct), # slippage ) ) @@ -401,7 +401,7 @@ async def send_stake_extrinsic( return False stake_coroutines = [ - send_stake_extrinsic(ni, am, curr, staking_address) + add_stake_extrinsic(ni, am, curr, staking_address) for i, (ni, am, curr) in enumerate( zip(netuids, amounts_to_stake, current_stake_balances) ) From 5cc2a35f3da51d8b9f32f193d216f3113d3fa7ec Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 5 Feb 2025 20:22:45 +0200 Subject: [PATCH 282/332] Add in childkey completion block for setting childkeys. --- bittensor_cli/src/bittensor/chain_data.py | 4 +- .../src/commands/stake/children_hotkeys.py | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 859a2cd6..68c7c903 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -179,9 +179,7 @@ def _fix_decoded( max_validators=decoded.get("max_validators"), adjustment_alpha=decoded.get("adjustment_alpha"), difficulty=decoded.get("difficulty"), - commit_reveal_period=decoded.get( - "commit_reveal_weights_interval" - ), + commit_reveal_period=decoded.get("commit_reveal_weights_interval"), commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), alpha_high=decoded.get("alpha_high"), alpha_low=decoded.get("alpha_low"), diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index c9ae6487..8678faab 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -22,6 +22,33 @@ ) +async def get_childkey_completion_block( + subtensor: SubtensorInterface, netuid: int +) -> tuple[int, int]: + """ + Calculates the block at which the childkey set request will complete + """ + blocks_since_last_step_query = subtensor.query( + "SubtensorModule", + "BlocksSinceLastStep", + params=[netuid], + ) + tempo_query = subtensor.get_hyperparameter( + param_name="Tempo", + netuid=netuid, + ) + block_number, blocks_since_last_step, tempo = await asyncio.gather( + subtensor.substrate.get_block_number(), + blocks_since_last_step_query, + tempo_query, + ) + cooldown = block_number + 7200 + blocks_left_in_tempo = tempo - blocks_since_last_step + next_tempo = block_number + blocks_left_in_tempo + next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown + return block_number, next_epoch_after_cooldown + + async def set_children_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, @@ -513,8 +540,14 @@ async def set_children( # Result if success: if wait_for_inclusion and wait_for_finalization: - console.print("New Status:") - await get_children(wallet, subtensor, netuid) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid + ) + console.print( + f"Your childkey request has been submitted. It will be completed around block {completion_block}, " + f"assuming you have the required key swap cost (default: 0.1 Tao) in your coldkey at that time. " + f"The current block is {current_block}" + ) console.print( ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" ) @@ -525,20 +558,28 @@ async def set_children( else: # set children on all subnets that parent is registered on netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: # dont include root network + for netuid_ in netuids: + if netuid_ == 0: # dont include root network continue - console.print(f"Setting children on netuid {netuid}.") + console.print(f"Setting children on netuid {netuid_}.") await set_children_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid_, hotkey=hotkey, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, ) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid_ + ) + console.print( + f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " + f"block {completion_block}, assuming you have the required key swap cost (default: 0.1 Tao) in your " + f"coldkey at that time. The current block is {current_block}." + ) console.print( ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" ) From 2b226883015990b1df5e1fabab127150068cba45 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 5 Feb 2025 21:05:59 +0200 Subject: [PATCH 283/332] Updates requirement. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4003d27..64017189 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface==1.0.0rc9 +async-substrate-interface==1.0.0rc10 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 From 32d19f15b3a3516d52349d92f99127aaaa4228cf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 11:44:10 -0800 Subject: [PATCH 284/332] Bumps btwallet --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64017189..ce6a1018 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ rich~=13.7 scalecodec==1.2.11 typer~=0.12 websockets>=14.1 -bittensor-wallet>=3.0.0 +bittensor-wallet>=3.0.1 plotille pywry plotly \ No newline at end of file From e8c718e6049129b27d91822d4bb809aa95e082f4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 12:47:48 -0800 Subject: [PATCH 285/332] Bumps requirements for btwallet (3.0.2) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ce6a1018..9d33a867 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ rich~=13.7 scalecodec==1.2.11 typer~=0.12 websockets>=14.1 -bittensor-wallet>=3.0.1 +bittensor-wallet>=3.0.2 plotille pywry plotly \ No newline at end of file From 20db6ce3680381adccc3ad0dacaad3aa21d41ee6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 13:46:59 -0800 Subject: [PATCH 286/332] Updates identity, sn identity, and other chain stuff --- bittensor_cli/cli.py | 31 ++++++++++-- bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 12 ++++- .../src/bittensor/subtensor_interface.py | 4 +- bittensor_cli/src/bittensor/utils.py | 47 ++++++++++++++++--- bittensor_cli/src/commands/subnets/subnets.py | 18 ++++++- bittensor_cli/src/commands/wallets.py | 10 ++-- bittensor_cli/src/commands/weights.py | 2 +- 8 files changed, 104 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 074ad874..bea9f0cc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2340,7 +2340,7 @@ def wallet_set_id( "--image", help="The image URL for the identity.", ), - discord_handle: str = typer.Option( + discord: str = typer.Option( "", "--discord", help="The Discord handle for the identity.", @@ -2350,11 +2350,16 @@ def wallet_set_id( "--description", help="The description for the identity.", ), - additional_info: str = typer.Option( + additional: str = typer.Option( "", "--additional", help="Additional details for the identity.", ), + github_repo: str = typer.Option( + "", + "--github", + help="The GitHub repository for the identity.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -2407,9 +2412,10 @@ def wallet_set_id( name, web_url, image_url, - discord_handle, + discord, description, - additional_info, + additional, + github_repo, ) return self._run_command( @@ -2422,6 +2428,7 @@ def wallet_set_id( identity["discord"], identity["description"], identity["additional"], + identity["github_repo"], prompt, ) ) @@ -4103,6 +4110,18 @@ def subnets_create( "--email", help="Contact email for subnet", ), + subnet_url: Optional[str] = typer.Option( + None, "--subnet-url", "--url", help="Subnet URL" + ), + discord: Optional[str] = typer.Option( + None, "--discord-handle", "--discord", help="Discord handle" + ), + description: Optional[str] = typer.Option( + None, "--description", help="Description" + ), + additional_info: Optional[str] = typer.Option( + None, "--additional-info", help="Additional information" + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4130,6 +4149,10 @@ def subnets_create( subnet_name=subnet_name, github_repo=github_repo, subnet_contact=subnet_contact, + subnet_url=subnet_url, + discord=discord, + description=description, + additional=additional_info, ) success = self._run_command( subnets.create(wallet, self.initialize_chain(network), identity, prompt), diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6d3b805c..92a00caa 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -646,7 +646,7 @@ class WalletValidationTypes(Enum): "kappa": "sudo_set_kappa", "difficulty": "sudo_set_difficulty", "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_period": "sudo_set_commit_reveal_weights_interval", + "commit_reveal_period": "sudo_set_commit_reveal_period", "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", "alpha_values": "sudo_set_alpha_values", "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 68c7c903..3b5a4c89 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -179,7 +179,7 @@ def _fix_decoded( max_validators=decoded.get("max_validators"), adjustment_alpha=decoded.get("adjustment_alpha"), difficulty=decoded.get("difficulty"), - commit_reveal_period=decoded.get("commit_reveal_weights_interval"), + commit_reveal_period=decoded.get("commit_reveal_period"), commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), alpha_high=decoded.get("alpha_high"), alpha_low=decoded.get("alpha_low"), @@ -574,7 +574,7 @@ def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": str(int(netuid)): u16_normalized_float(int(req)) for (netuid, req) in decoded.get("network_connect") }, - emission_value=decoded.get("emission_values"), + emission_value=decoded.get("emission_value"), burn=Balance.from_rao(decoded.get("burn")), owner_ss58=decode_account_id(decoded.get("owner")), ) @@ -587,6 +587,10 @@ class SubnetIdentity(InfoBase): subnet_name: str github_repo: str subnet_contact: str + subnet_url: str + discord: str + description: str + additional: str @classmethod def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": @@ -594,6 +598,10 @@ def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": subnet_name=bytes(decoded["subnet_name"]).decode(), github_repo=bytes(decoded["github_repo"]).decode(), subnet_contact=bytes(decoded["subnet_contact"]).decode(), + subnet_url=bytes(decoded["subnet_url"]).decode(), + discord=bytes(decoded["discord"]).decode(), + description=bytes(decoded["description"]).decode(), + additional=bytes(decoded["additional"]).decode(), ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 5b0d6c9a..8c464062 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -839,7 +839,7 @@ async def query_all_identities( identities = await self.substrate.query_map( module="SubtensorModule", - storage_function="Identities", + storage_function="IdentitiesV2", block_hash=block_hash, reuse_block_hash=reuse_block, ) @@ -877,7 +877,7 @@ async def query_identity( """ identity_info = await self.query( module="SubtensorModule", - storage_function="Identities", + storage_function="IdentitiesV2", params=[key], block_hash=block_hash, reuse_block_hash=reuse_block, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 5e8091a7..11dbdeab 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1039,9 +1039,10 @@ def prompt_for_identity( name: Optional[str], web_url: Optional[str], image_url: Optional[str], - discord_handle: Optional[str], + discord: Optional[str], description: Optional[str], - additional_info: Optional[str], + additional: Optional[str], + github_repo: Optional[str], ): """ Prompts the user for identity fields with validation. @@ -1053,9 +1054,10 @@ def prompt_for_identity( ("name", "[blue]Display name[/blue]", name), ("url", "[blue]Web URL[/blue]", web_url), ("image", "[blue]Image URL[/blue]", image_url), - ("discord", "[blue]Discord handle[/blue]", discord_handle), + ("discord", "[blue]Discord handle[/blue]", discord), ("description", "[blue]Description[/blue]", description), - ("additional", "[blue]Additional information[/blue]", additional_info), + ("additional", "[blue]Additional information[/blue]", additional), + ("github_repo", "[blue]GitHub repository URL[/blue]", github_repo), ] text_rejection = partial( @@ -1069,9 +1071,10 @@ def prompt_for_identity( name, web_url, image_url, - discord_handle, + discord, description, - additional_info, + additional, + github_repo, ] ): console.print( @@ -1096,6 +1099,10 @@ def prompt_for_subnet_identity( subnet_name: Optional[str], github_repo: Optional[str], subnet_contact: Optional[str], + subnet_url: Optional[str], + discord: Optional[str], + description: Optional[str], + additional: Optional[str], ): """ Prompts the user for required subnet identity fields with validation. @@ -1133,6 +1140,34 @@ def prompt_for_subnet_identity( lambda x: x and not is_valid_contact(x), "[red]Error:[/red] Please enter a valid email address.", ), + ( + "subnet_url", + "[blue]Subnet URL [dim](optional)[/blue]", + subnet_url, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Please enter a valid URL.", + ), + ( + "discord", + "[blue]Discord handle [dim](optional)[/blue]", + discord, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Please enter a valid Discord handle.", + ), + ( + "description", + "[blue]Description [dim](optional)[/blue]", + description, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Description must be <= 64 raw bytes.", + ), + ( + "additional", + "[blue]Additional information [dim](optional)[/blue]", + additional, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Additional information must be <= 64 raw bytes.", + ), ] for key, prompt, value, rejection_func, rejection_msg in fields: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index b68f9892..c7a8f7bc 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -122,6 +122,18 @@ async def _find_event_attributes_in_extrinsic_receipt( "subnet_contact": subnet_identity["subnet_contact"].encode() if subnet_identity.get("subnet_contact") else b"", + "subnet_url": subnet_identity["subnet_url"].encode() + if subnet_identity.get("subnet_url") + else b"", + "discord": subnet_identity["discord"].encode() + if subnet_identity.get("discord") + else b"", + "description": subnet_identity["description"].encode() + if subnet_identity.get("description") + else b"", + "additional": subnet_identity["additional"].encode() + if subnet_identity.get("additional") + else b"", } for field, value in identity_data.items(): max_size = 64 # bytes @@ -1391,9 +1403,10 @@ async def create( name=None, web_url=None, image_url=None, - discord_handle=None, + discord=None, description=None, - additional_info=None, + additional=None, + github_repo=None, ) await set_id( @@ -1405,6 +1418,7 @@ async def create( identity["discord"], identity["description"], identity["additional"], + identity["github_repo"], prompt, ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 5d9217d5..4d0b339b 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1296,9 +1296,10 @@ async def set_id( name: str, web_url: str, image_url: str, - discord_handle: str, + discord: str, description: str, - additional_info: str, + additional: str, + github_repo: str, prompt: bool, ): """Create a new or update existing identity on-chain.""" @@ -1307,9 +1308,10 @@ async def set_id( "name": name.encode(), "url": web_url.encode(), "image": image_url.encode(), - "discord": discord_handle.encode(), + "discord": discord.encode(), "description": description.encode(), - "additional": additional_info.encode(), + "additional": additional.encode(), + "github_repo": github_repo.encode(), } for field, value in identity_data.items(): diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index caf5e72f..df381582 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -148,7 +148,7 @@ async def _commit_reveal( ) -> tuple[bool, str]: interval = int( await self.subtensor.get_hyperparameter( - param_name="get_commit_reveal_weights_interval", + param_name="get_commit_reveal_period", netuid=self.netuid, reuse_block=False, ) From 596301f2bc046064dc8c0f8a565858c7ee3f5213 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 13:58:55 -0800 Subject: [PATCH 287/332] Updates sudo for cr interval setting --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 92a00caa..6d3b805c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -646,7 +646,7 @@ class WalletValidationTypes(Enum): "kappa": "sudo_set_kappa", "difficulty": "sudo_set_difficulty", "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_period": "sudo_set_commit_reveal_period", + "commit_reveal_period": "sudo_set_commit_reveal_weights_interval", "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", "alpha_values": "sudo_set_alpha_values", "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", From d897059bea49c164cdf3c46168f61cae58102f16 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 14:19:52 -0800 Subject: [PATCH 288/332] Updates min stake fee --- bittensor_cli/src/commands/stake/move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 68211a5f..72b1d802 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -MIN_STAKE_FEE = Balance.from_rao(500_000) +MIN_STAKE_FEE = Balance.from_rao(50_000) # Helpers From ff0ceb23c9c1c7eb1866e1e70e9f614e891fa6ca Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 14:20:22 -0800 Subject: [PATCH 289/332] Updates cooldown fee for chk --- bittensor_cli/src/commands/stake/children_hotkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 8678faab..85743034 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -42,7 +42,7 @@ async def get_childkey_completion_block( blocks_since_last_step_query, tempo_query, ) - cooldown = block_number + 7200 + cooldown = block_number + 1 blocks_left_in_tempo = tempo - blocks_since_last_step next_tempo = block_number + blocks_left_in_tempo next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown From bb5e56bda966777a96be9e31856e2d2d480449d2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 5 Feb 2025 14:35:25 -0800 Subject: [PATCH 290/332] Updates requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d33a867..21ae8c28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface==1.0.0rc10 +async-substrate-interface>=1.0.0rc10 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 From 12d330cc4bb763f8838968e5d2b3261554776e63 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 6 Feb 2025 09:58:53 -0500 Subject: [PATCH 291/332] oops don't mention cost on chk swap --- bittensor_cli/src/commands/stake/children_hotkeys.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 85743034..0cee531c 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -544,8 +544,7 @@ async def set_children( subtensor, netuid ) console.print( - f"Your childkey request has been submitted. It will be completed around block {completion_block}, " - f"assuming you have the required key swap cost (default: 0.1 Tao) in your coldkey at that time. " + f"Your childkey request has been submitted. It will be completed around block {completion_block}. " f"The current block is {current_block}" ) console.print( From 3d1593f9fabc1307e630e609662ed8e959174950 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 6 Feb 2025 10:17:13 -0500 Subject: [PATCH 292/332] remove 2nd mention --- bittensor_cli/src/commands/stake/children_hotkeys.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 0cee531c..0f978d83 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -576,8 +576,7 @@ async def set_children( ) console.print( f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " - f"block {completion_block}, assuming you have the required key swap cost (default: 0.1 Tao) in your " - f"coldkey at that time. The current block is {current_block}." + f"block {completion_block}. The current block is {current_block}." ) console.print( ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" From d1c59a54948827014983a5c67ea38252a93c4349 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 6 Feb 2025 11:33:00 -0800 Subject: [PATCH 293/332] Adds recycle cost to s show --- bittensor_cli/src/commands/subnets/subnets.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c7a8f7bc..59003eb1 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -810,16 +810,17 @@ async def show( prompt: bool = True, ) -> Optional[str]: async def show_root(): - all_subnets = await subtensor.all_subnets() + block_hash = await subtensor.substrate.get_chain_head() + all_subnets = await subtensor.all_subnets(block_hash=block_hash) root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: print_error("The root subnet does not exist") raise typer.Exit() root_state, identities, old_identities = await asyncio.gather( - subtensor.get_subnet_state(netuid=0), - subtensor.query_all_identities(), - subtensor.get_delegate_identities(), + subtensor.get_subnet_state(netuid=0, block_hash=block_hash), + subtensor.query_all_identities(block_hash=block_hash), + subtensor.get_delegate_identities(block_hash=block_hash), ) if root_state is None: @@ -1031,16 +1032,21 @@ async def show_subnet(netuid_: int): if not await subtensor.subnet_exists(netuid=netuid): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") raise typer.Exit() + block_hash = await subtensor.substrate.get_chain_head() ( subnet_info, subnet_state, identities, old_identities, + current_burn_cost, ) = await asyncio.gather( - subtensor.subnet(netuid=netuid_), - subtensor.get_subnet_state(netuid=netuid_), - subtensor.query_all_identities(), - subtensor.get_delegate_identities(), + subtensor.subnet(netuid=netuid_, block_hash=block_hash), + subtensor.get_subnet_state(netuid=netuid_, block_hash=block_hash), + subtensor.query_all_identities(block_hash=block_hash), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_hyperparameter( + param_name="Burn", netuid=netuid_, block_hash=block_hash + ), ) if subnet_state is None: print_error(f"Subnet {netuid_} does not exist") @@ -1281,6 +1287,11 @@ async def show_subnet(netuid_: int): if not verbose else f"{subnet_info.alpha_in.tao:,.4f}" ) + current_registration_burn = ( + Balance.from_rao(int(current_burn_cost)) + if current_burn_cost + else Balance(0) + ) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" @@ -1291,6 +1302,7 @@ async def show_subnet(netuid_: int): f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Registration cost (recycled): [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]τ {current_registration_burn.tao:.4f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" ) # console.print( # """ From f9df8835361f59f4360235b96235d7e32b99f91a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 6 Feb 2025 20:04:07 -0800 Subject: [PATCH 294/332] Adds safe staking configs, impl in stake add --- bittensor_cli/cli.py | 228 ++++++++++++++--- bittensor_cli/src/__init__.py | 1 + bittensor_cli/src/bittensor/utils.py | 14 ++ bittensor_cli/src/commands/stake/add.py | 293 ++++++++++++++++++---- bittensor_cli/src/commands/stake/stake.py | 7 +- 5 files changed, 464 insertions(+), 79 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1c0bc65c..67dec15c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -243,6 +243,26 @@ class Options: help="Create wallet from uri (e.g. 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve')", callback=validate_uri, ) + slippage_tolerance = typer.Option( + None, + "--slippage", + "--slippage-tolerance", + "--tolerance", + help="Set the slippage tolerance percentage for transactions (default: 0.05%).", + ) + safe_staking = typer.Option( + None, + "--safe-staking/--no-safe-staking", + "--safe/--unsafe", + help="Enable or disable safe staking mode (default: enabled).", + ) + allow_partial_stake = typer.Option( + None, + "--allow-partial-stake/--no-allow-partial-stake", + "--partial/--no-partial", + "--allow/--not-allow", + help="Enable or disable partial stake mode (default: disabled).", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -517,25 +537,29 @@ def __init__(self): "wallet_hotkey": None, "network": None, "use_cache": True, - "metagraph_cols": { - "UID": True, - "GLOBAL_STAKE": True, - "LOCAL_STAKE": True, - "STAKE_WEIGHT": True, - "RANK": True, - "TRUST": True, - "CONSENSUS": True, - "INCENTIVE": True, - "DIVIDENDS": True, - "EMISSION": True, - "VTRUST": True, - "VAL": True, - "UPDATED": True, - "ACTIVE": True, - "AXON": True, - "HOTKEY": True, - "COLDKEY": True, - }, + "slippage_tolerance": None, + "safe_staking": True, + "allow_partial_stake": False, + # Commenting this out as this needs to get updated + # "metagraph_cols": { + # "UID": True, + # "GLOBAL_STAKE": True, + # "LOCAL_STAKE": True, + # "STAKE_WEIGHT": True, + # "RANK": True, + # "TRUST": True, + # "CONSENSUS": True, + # "INCENTIVE": True, + # "DIVIDENDS": True, + # "EMISSION": True, + # "VTRUST": True, + # "VAL": True, + # "UPDATED": True, + # "ACTIVE": True, + # "AXON": True, + # "HOTKEY": True, + # "COLDKEY": True, + # }, } self.subtensor = None self.config_base_path = os.path.expanduser(defaults.config.base_path) @@ -634,7 +658,7 @@ def __init__(self): self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) self.config_app.command("clear")(self.del_config) - self.config_app.command("metagraph")(self.metagraph_config) + self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands self.wallet_app.command( @@ -1057,6 +1081,25 @@ def set_config( help="Disable caching of some commands. This will disable the `--reuse-last` and `--html` flags on " "commands such as `subnets metagraph`, `stake show` and `subnets list`.", ), + slippage_tolerance: Optional[float] = typer.Option( + None, + "--slippage", + "--slippage-tolerance", + "--tolerance", + help="Set the slippage tolerance percentage for transactions (e.g. 0.1 for 0.1%).", + ), + safe_staking: Optional[bool] = typer.Option( + None, + "--safe-staking/--no-safe-staking", + "--safe/--unsafe", + help="Enable or disable safe staking mode.", + ), + allow_partial_stake: Optional[bool] = typer.Option( + None, + "--allow-partial-stake/--no-allow-partial-stake", + "--partial/--no-partial", + "--allow/--not-allow", + ), ): """ Sets the values in the config file. To set the metagraph configuration, use the command `btcli config metagraph` @@ -1067,8 +1110,11 @@ def set_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, + "slippage_tolerance": slippage_tolerance, + "safe_staking": safe_staking, + "allow_partial_stake": allow_partial_stake, } - bools = ["use_cache"] + bools = ["use_cache", "safe_staking", "allow_partial_stake"] if all(v is None for v in args.values()): # Print existing configs self.get_config() @@ -1092,6 +1138,17 @@ def set_config( default=True, ) self.config[arg] = nc + + elif arg == "slippage_tolerance": + val = FloatPrompt.ask( + f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)", + default=0.05, + ) + if val < 0 or val > 1: + print_error("Slippage tolerance must be between 0 and 1.") + raise typer.Exit() + self.config[arg] = val + else: val = Prompt.ask( f"What value would you like to assign to [red]{arg}[/red]?" @@ -1149,6 +1206,18 @@ def del_config( wallet_hotkey: bool = typer.Option(False, *Options.wallet_hotkey.param_decls), network: bool = typer.Option(False, *Options.network.param_decls), use_cache: bool = typer.Option(False, "--cache"), + slippage_tolerance: bool = typer.Option( + False, "--slippage", "--slippage-tolerance", "--tolerance" + ), + safe_staking: bool = typer.Option( + False, "--safe-staking/--no-safe-staking", "--safe/--unsafe" + ), + allow_partial_stake: bool = typer.Option( + False, + "--allow-partial-stake/--no-allow-partial-stake", + "--partial/--no-partial", + "--allow/--not-allow", + ), all_items: bool = typer.Option(False, "--all"), ): """ @@ -1178,6 +1247,9 @@ def del_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, + "slippage_tolerance": slippage_tolerance, + "safe_staking": safe_staking, + "allow_partial_stake": allow_partial_stake, } # If no specific argument is provided, iterate over all @@ -1239,6 +1311,8 @@ def get_config(self): else: if value in Constants.networks: value = value + f" ({Constants.network_map[value]})" + if key == "slippage_tolerance": + value = f"{value} ({value*100}%)" elif key in deprecated_configs: continue @@ -1251,13 +1325,97 @@ def get_config(self): table.add_row(str(key), str(value), "") console.print(table) - console.print( - dedent( - """ - [red]Deprecation notice[/red]: The chain endpoint config is now deprecated. You can use the network config to pass chain endpoints. - """ + + def ask_slippage( + self, + slippage_tolerance: Optional[float], + ) -> float: + """ + Gets slippage tolerance from args, config, or default. + + Args: + slippage_tolerance (Optional[float]): Explicitly provided slippage value + + Returns: + float: Slippage tolerance value + """ + if slippage_tolerance is not None: + console.print( + f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{slippage_tolerance} ({slippage_tolerance*100}%)[/bold cyan]." ) - ) + return slippage_tolerance + elif self.config.get("slippage_tolerance") is not None: + config_slippage = self.config["slippage_tolerance"] + console.print( + f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{config_slippage} ({config_slippage*100}%)[/bold cyan] (from config)." + ) + return config_slippage + else: + console.print( + f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{defaults.slippage_tolerance} ({defaults.slippage_tolerance*100}%)[/bold cyan] by default." + ) + return defaults.slippage_tolerance + + def ask_safe_staking( + self, + safe_staking: Optional[bool], + ) -> bool: + """ + Gets safe staking setting from args, config, or default. + + Args: + safe_staking (Optional[bool]): Explicitly provided safe staking value + + Returns: + bool: Safe staking setting + """ + if safe_staking is not None: + console.print( + f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan]." + ) + return safe_staking + elif self.config.get("safe_staking") is not None: + safe_staking = self.config["safe_staking"] + console.print( + f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] (from config)." + ) + return safe_staking + else: + safe_staking = True + console.print( + f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] by default." + ) + return safe_staking + + def ask_partial_stake( + self, + allow_partial_stake: Optional[bool], + ) -> bool: + """ + Gets partial stake setting from args, config, or default. + + Args: + allow_partial_stake (Optional[bool]): Explicitly provided partial stake value + + Returns: + bool: Partial stake setting + """ + if allow_partial_stake is not None: + console.print( + f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan]." + ) + return allow_partial_stake + elif self.config.get("allow_partial_stake") is not None: + config_partial = self.config["allow_partial_stake"] + console.print( + f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if config_partial else 'disabled'}[/bold cyan] (from config)." + ) + return config_partial + else: + console.print( + f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] by default." + ) + return False def wallet_ask( self, @@ -2631,6 +2789,9 @@ def stake_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + slippage_tolerance: Optional[float] = Options.slippage_tolerance, + safe_staking: Optional[bool] = Options.safe_staking, + allow_partial_stake: Optional[bool] = Options.allow_partial_stake, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2647,6 +2808,10 @@ def stake_add( [green]$[/green] btcli stake add --amount 100 --wallet-name --wallet-hotkey """ self.verbosity_handler(quiet, verbose) + safe_staking = self.ask_safe_staking(safe_staking) + if safe_staking: + allow_partial_stake = self.ask_partial_stake(allow_partial_stake) + slippage_tolerance = self.ask_slippage(slippage_tolerance) netuid = get_optional_netuid(netuid, all_netuids) if stake_all and amount: @@ -2795,6 +2960,9 @@ def stake_add( all_hotkeys, included_hotkeys, excluded_hotkeys, + safe_staking, + slippage_tolerance, + allow_partial_stake, ) ) @@ -2873,9 +3041,9 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) - # TODO: Coldkey related unstakes need to be updated. Patching for now. - unstake_all_alpha = False - unstake_all = False + # # TODO: Coldkey related unstakes need to be updated. Patching for now. + # unstake_all_alpha = False + # unstake_all = False if interactive and any( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6d3b805c..821fe5e0 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -65,6 +65,7 @@ def decode(key: str, default=""): class Defaults: netuid = 1 + slippage_tolerance = 0.005 class config: base_path = "~/.bittensor" diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 5e8091a7..09c79d9b 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1226,3 +1226,17 @@ def print_linux_dependency_message(): def is_linux(): """Returns True if the operating system is Linux.""" return platform.system().lower() == "linux" + +def validate_slippage_tolerance(value: Optional[float]) -> Optional[float]: + """Validates slippage tolerance input""" + if value is not None: + if value < 0: + raise typer.BadParameter("Slippage tolerance cannot be negative (less than 0%).") + if value > 1: + raise typer.BadParameter("Slippage tolerance cannot be greater than 1 (100%).") + if value > 0.5: + console.print( + f"[yellow]Warning: High slippage tolerance of {value*100}% specified. " + "This may result in unfavorable transaction execution.[/yellow]" + ) + return value diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index bc3dae98..910c3915 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -4,7 +4,7 @@ import typer from typing import TYPE_CHECKING, Optional from rich.table import Table -from rich.prompt import Confirm +from rich.prompt import Confirm, Prompt from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE @@ -17,7 +17,6 @@ is_valid_ss58_address, print_error, print_verbose, - prompt_stake_amount, ) from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError @@ -25,6 +24,49 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +# Helper functions +def prompt_stake_amount( + current_balance: Balance, netuid: int, action_name: str +) -> tuple[Balance, bool]: + """Prompts user to input a stake amount with validation. + + Args: + current_balance (Balance): The maximum available balance + netuid (int): The subnet id to get the correct unit + action_name (str): The name of the action (e.g. "transfer", "move", "unstake") + + Returns: + tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected) + """ + while True: + amount_input = Prompt.ask( + f"\nEnter the amount to {action_name}" + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"or " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"for entire balance" + ) + + if amount_input.lower() == "all": + return current_balance, True + + try: + amount = float(amount_input) + if amount <= 0: + console.print("[red]Amount must be greater than 0[/red]") + continue + if amount > current_balance.tao: + console.print( + f"[red]Amount exceeds available balance of " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"[/red]" + ) + continue + return Balance.from_tao(amount), False + except ValueError: + console.print("[red]Please enter a valid number or 'all'[/red]") + def _get_hotkeys_to_stake_to( wallet: Wallet, @@ -80,7 +122,12 @@ def _get_hotkeys_to_stake_to( return [(None, wallet.hotkey.ss58_address)] -def define_stake_stable(wallet: Wallet, subtensor: "SubtensorInterface") -> Table: +def define_stake_table( + wallet: Wallet, + subtensor: "SubtensorInterface", + safe_staking: bool, + slippage_tolerance: float, +) -> Table: """Creates and initializes a table for displaying stake information. Args: @@ -128,10 +175,21 @@ def define_stake_stable(wallet: Wallet, subtensor: "SubtensorInterface") -> Tabl "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] ) + if safe_staking: + table.add_column( + f"Rate with tolerance: [blue]({slippage_tolerance*100}%)[/blue]", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Partial stake enabled", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) return table -def print_table_and_slippage(table: Table, max_slippage: float): +def print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool): """Prints the stake table, slippage warning, and table description. Args: @@ -148,8 +206,7 @@ def print_table_and_slippage(table: Table, max_slippage: float): console.print(message) # Table description - console.print( - """ + base_description = """ [bold white]Description[/bold white]: The table displays information about the stake operation you are about to perform. The columns are as follows: @@ -158,14 +215,16 @@ def print_table_and_slippage(table: Table, max_slippage: float): - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). -""" - ) + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" + + safe_staking_description = """ + - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. + - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" + console.print(base_description + (safe_staking_description if safe_staking else "")) -def calculate_slippage( - subnet_info, amount: Balance -) -> tuple[Balance, str, float]: + +def calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: """Calculate slippage when adding stake. Args: @@ -192,6 +251,7 @@ def calculate_slippage( return received_amount, slippage_str, slippage_pct_float, rate +# Command async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", @@ -202,9 +262,11 @@ async def stake_add( all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], + safe_staking: bool, + slippage_tolerance: float, + allow_partial_stake: bool, ): """ - Args: wallet: wallet object subtensor: SubtensorInterface object @@ -217,12 +279,94 @@ async def stake_add( all_hotkeys: whether to stake all hotkeys include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) + safe_staking: whether to use safe staking + slippage_tolerance: slippage tolerance percentage for stake operations + allow_partial_stake: whether to allow partial stake Returns: bool: True if stake operation is successful, False otherwise """ + async def safe_stake_extrinsic( + netuid: int, + amount: Balance, + current_stake: Balance, + hotkey_ss58: str, + price_limit: Balance, + wallet: Wallet, + subtensor: "SubtensorInterface", + status=None + ) -> None: + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid}" + ) - async def add_stake_extrinsic( + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.coldkeypub.ss58_address + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce + ) + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + except SubstrateRequestException as e: + err_out( + f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + ) + return + else: + await response.process_events() + if not await response.is_success: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + ) + else: + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + ), + ) + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + + amount_staked = current_wallet_balance - new_balance + if allow_partial_stake and (amount_staked != amount): + console.print( + "Partial stake transaction. Staked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount}[/blue]" + ) + + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current_stake}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + + async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None ): err_out = partial(print_error, status=status) @@ -232,6 +376,7 @@ async def add_stake_extrinsic( next_nonce = await subtensor.substrate.get_account_next_index( wallet.coldkeypub.ss58_address ) + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -260,20 +405,14 @@ async def add_stake_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: - new_balance, stake_info_dict = await asyncio.gather( + new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkey( + subtensor.get_stake( + hotkey_ss58=staking_address_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_i, ), ) - new_stake = Balance.from_rao(0) - for stake_info in stake_info_dict: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break console.print( f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" ) @@ -330,14 +469,15 @@ async def add_stake_extrinsic( rows = [] amounts_to_stake = [] current_stake_balances = [] + prices_with_tolerance = [] remaining_wallet_balance = current_wallet_balance max_slippage = 0.0 for hotkey in hotkeys_to_stake_to: for netuid in netuids: # Check that the subnet exists. - dynamic_info = all_subnets.get(netuid) - if not dynamic_info: + subnet_info = all_subnets.get(netuid) + if not subnet_info: err_console.print(f"Subnet with netuid: {netuid} does not exist.") continue current_stake_balances.append(hotkey_stake_map[hotkey[1]][netuid]) @@ -367,44 +507,101 @@ async def add_stake_extrinsic( # Calculate slippage received_amount, slippage_pct, slippage_pct_float, rate = ( - calculate_slippage(dynamic_info, amount_to_stake) + calculate_slippage(subnet_info, amount_to_stake) ) max_slippage = max(slippage_pct_float, max_slippage) # Add rows for the table - rows.append( - ( - str(netuid), # netuid - f"{hotkey[1]}", # hotkey - str(amount_to_stake), # amount - rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate - str(received_amount.set_unit(netuid)), # received - str(slippage_pct), # slippage + base_row = [ + str(netuid), # netuid + f"{hotkey[1]}", # hotkey + str(amount_to_stake), # amount + str(rate) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate + str(received_amount.set_unit(netuid)), # received + str(slippage_pct), # slippage + ] + + # If we are staking safe, add price tolerance + if safe_staking: + if subnet_info.is_dynamic: + rate = 1 / subnet_info.price.tao or 1 + rate_with_tolerance = rate * (1 + slippage_tolerance) # Rate only for display + price_with_tolerance = subnet_info.price.rao * (1 + slippage_tolerance) # Actual price to pass to extrinsic + else: + rate_with_tolerance = 1 + price_with_tolerance = Balance.from_rao(1) + prices_with_tolerance.append(price_with_tolerance) + + base_row.extend( + [ + str(rate_with_tolerance) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # safe staking + ] ) - ) + + rows.append(tuple(base_row)) # Define and print stake table + slippage warning - table = define_stake_stable(wallet, subtensor) + table = define_stake_table( + wallet, subtensor, safe_staking, slippage_tolerance + ) for row in rows: table.add_row(*row) - print_table_and_slippage(table, max_slippage) + print_table_and_slippage(table, max_slippage, safe_staking) if prompt: if not Confirm.ask("Would you like to continue?"): raise typer.Exit() - - # Perform staking operation. try: wallet.unlock_coldkey() except KeyFileError: err_console.print("Error decrypting coldkey (possibly incorrect password)") return False - stake_coroutines = [ - add_stake_extrinsic(ni, am, curr, staking_address) - for i, (ni, am, curr) in enumerate( - zip(netuids, amounts_to_stake, current_stake_balances) - ) - for _, staking_address in hotkeys_to_stake_to - ] - await asyncio.gather(*stake_coroutines) + if safe_staking: + stake_coroutines = [] + for i, (ni, am, curr, price_with_tolerance) in enumerate( + zip(netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance) + ): + for _, staking_address in hotkeys_to_stake_to: + # Regular extrinsic for root subnet + if ni == 0: + stake_coroutines.append( + stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + ) + ) + else: + stake_coroutines.append( + safe_stake_extrinsic( + netuid=ni, + amount=am, + current_stake=curr, + hotkey_ss58=staking_address, + price_limit=price_with_tolerance, + wallet=wallet, + subtensor=subtensor, + ) + ) + else: + stake_coroutines = [ + stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + ) + for i, (ni, am, curr) in enumerate( + zip(netuids, amounts_to_stake, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + ] + + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): + await asyncio.gather(*stake_coroutines) + \ No newline at end of file diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 68aef724..b94b08ed 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -293,7 +293,12 @@ async def _unstake_all( else "Unstaking Summary - All Alpha Stakes" ) table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + ), show_footer=True, show_edge=False, header_style="bold white", From 81c13a963a77d1507993cb5dd0739494734e8d4a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 6 Feb 2025 22:44:02 -0800 Subject: [PATCH 295/332] Bumps version, changelog ,and err handling --- CHANGELOG.md | 6 ++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 4 ++-- requirements.txt | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e540255..41bf79c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 9.0.0rc2 /2025-02-06 + +## What's Changed +* Updates to new Async Substrate Interface +* Other bug fixes and improvements + ## 9.0.0rc1 /2025-02-04 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 32f17677..7f7f379d 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0rc1" +__version__ = "9.0.0rc2" __all__ = [CLIManager, __version__] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bea9f0cc..99841558 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -65,7 +65,7 @@ class GitError(Exception): pass -__version__ = "9.0.0rc1" +__version__ = "9.0.0rc2" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -937,7 +937,7 @@ async def _run(): try: raise typer.Exit() except Exception as e: # ensures we always exit cleanly - if not isinstance(e, typer.Exit): + if not isinstance(e, (typer.Exit, RuntimeError)): # temporarily to handle multiple run commands in one session err_console.print(f"An unknown error has occurred: {e}") if sys.version_info < (3, 10): diff --git a/requirements.txt b/requirements.txt index 21ae8c28..c7156e09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface>=1.0.0rc10 +async-substrate-interface>=1.0.0rc12 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 From 9948bc97f8e589628ae4522a7149c93a800106a6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 6 Feb 2025 22:52:08 -0800 Subject: [PATCH 296/332] Adds Latent to networks --- bittensor_cli/src/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6d3b805c..4cfa2ced 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -4,13 +4,14 @@ class Constants: - networks = ["local", "finney", "test", "archive", "rao", "dev"] + networks = ["local", "finney", "test", "archive", "rao", "dev", "latent-lite"] finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443" archive_entrypoint = "wss://archive.chain.opentensor.ai:443" rao_entrypoint = "wss://rao.chain.opentensor.ai:443" dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " local_entrypoint = "ws://127.0.0.1:9944" + latent_lite_entrypoint = "wss://lite.sub.latent.to:443" network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, @@ -18,6 +19,7 @@ class Constants: "local": local_entrypoint, "dev": dev_entrypoint, "rao": rao_entrypoint, + "latent-lite": latent_lite_entrypoint, } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" From e626b74a3fd03776a11367f93b25412b2bc56935 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Feb 2025 13:58:17 +0200 Subject: [PATCH 297/332] Uses uvloop if it's installed --- bittensor_cli/cli.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 99841558..a7ba78c8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio import curses +import importlib import os.path import re import ssl @@ -504,6 +505,7 @@ class CLIManager: subnets_app: typer.Typer weights_app: typer.Typer utils_app = typer.Typer(epilog=_epilog) + runner = asyncio def __init__(self): self.config = { @@ -937,15 +939,12 @@ async def _run(): try: raise typer.Exit() except Exception as e: # ensures we always exit cleanly - if not isinstance(e, (typer.Exit, RuntimeError)): # temporarily to handle multiple run commands in one session + if not isinstance( + e, (typer.Exit, RuntimeError) + ): # temporarily to handle multiple run commands in one session err_console.print(f"An unknown error has occurred: {e}") - if sys.version_info < (3, 10): - # For Python 3.9 or lower - return asyncio.get_event_loop().run_until_complete(_run()) - else: - # For Python 3.10 or higher - return asyncio.run(_run()) + return self.runner.run(_run()) def main_callback( self, @@ -996,6 +995,16 @@ def main_callback( if k in self.config.keys(): self.config[k] = v + try: + uvloop = importlib.import_module("uvloop") + if sys.version_info >= (3, 11): + self.runner = uvloop + else: + uvloop.install() + self.runner = asyncio + except ModuleNotFoundError: + self.runner = asyncio + def verbosity_handler(self, quiet: bool, verbose: bool): if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") From 7fb155f2c5ae7f2f207644a2cca403c005f54f48 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Feb 2025 20:13:22 +0200 Subject: [PATCH 298/332] Restored 3.9 functionality. --- bittensor_cli/cli.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a7ba78c8..dbe6cdf1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -505,7 +505,7 @@ class CLIManager: subnets_app: typer.Typer weights_app: typer.Typer utils_app = typer.Typer(epilog=_epilog) - runner = asyncio + asyncio_runner = asyncio def __init__(self): self.config = { @@ -944,7 +944,7 @@ async def _run(): ): # temporarily to handle multiple run commands in one session err_console.print(f"An unknown error has occurred: {e}") - return self.runner.run(_run()) + return self.asyncio_runner(_run()) def main_callback( self, @@ -995,15 +995,19 @@ def main_callback( if k in self.config.keys(): self.config[k] = v - try: - uvloop = importlib.import_module("uvloop") - if sys.version_info >= (3, 11): - self.runner = uvloop - else: - uvloop.install() - self.runner = asyncio - except ModuleNotFoundError: - self.runner = asyncio + if sys.version_info < (3, 10): + # For Python 3.9 or lower + self.asyncio_runner = asyncio.get_event_loop().run_until_complete + else: + try: + uvloop = importlib.import_module("uvloop") + if sys.version_info >= (3, 11): + self.asyncio_runner = uvloop.run + else: + uvloop.install() + self.asyncio_runner = asyncio.run + except ModuleNotFoundError: + self.asyncio_runner = asyncio def verbosity_handler(self, quiet: bool, verbose: bool): if quiet and verbose: From a43c451b407a21e727862ce5ea7bf300ffa25f36 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 12:33:11 -0800 Subject: [PATCH 299/332] slippage_tolerance -> price_tolerance + good error handling --- bittensor_cli/cli.py | 91 +++++++++++++------------ bittensor_cli/src/__init__.py | 2 +- bittensor_cli/src/bittensor/utils.py | 10 +-- bittensor_cli/src/commands/stake/add.py | 76 +++++++++++++-------- 4 files changed, 102 insertions(+), 77 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 67dec15c..107fa1e2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -55,9 +55,9 @@ prompt_for_subnet_identity, print_linux_dependency_message, is_linux, + validate_rate_tolerance, ) from typing_extensions import Annotated -from textwrap import dedent from websockets import ConnectionClosed, InvalidHandshake from yaml import safe_dump, safe_load @@ -243,12 +243,13 @@ class Options: help="Create wallet from uri (e.g. 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve')", callback=validate_uri, ) - slippage_tolerance = typer.Option( + rate_tolerance = typer.Option( None, "--slippage", "--slippage-tolerance", "--tolerance", - help="Set the slippage tolerance percentage for transactions (default: 0.05%).", + help="Set the rate tolerance percentage for transactions (default: 0.05%).", + callback=validate_rate_tolerance, ) safe_staking = typer.Option( None, @@ -261,6 +262,7 @@ class Options: "--allow-partial-stake/--no-allow-partial-stake", "--partial/--no-partial", "--allow/--not-allow", + "--allow-partial/--not-partial", help="Enable or disable partial stake mode (default: disabled).", ) @@ -537,7 +539,7 @@ def __init__(self): "wallet_hotkey": None, "network": None, "use_cache": True, - "slippage_tolerance": None, + "rate_tolerance": None, "safe_staking": True, "allow_partial_stake": False, # Commenting this out as this needs to get updated @@ -1081,12 +1083,12 @@ def set_config( help="Disable caching of some commands. This will disable the `--reuse-last` and `--html` flags on " "commands such as `subnets metagraph`, `stake show` and `subnets list`.", ), - slippage_tolerance: Optional[float] = typer.Option( + rate_tolerance: Optional[float] = typer.Option( None, "--slippage", "--slippage-tolerance", "--tolerance", - help="Set the slippage tolerance percentage for transactions (e.g. 0.1 for 0.1%).", + help="Set the rate tolerance percentage for transactions (e.g. 0.1 for 0.1%).", ), safe_staking: Optional[bool] = typer.Option( None, @@ -1110,7 +1112,7 @@ def set_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, - "slippage_tolerance": slippage_tolerance, + "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, } @@ -1139,16 +1141,19 @@ def set_config( ) self.config[arg] = nc - elif arg == "slippage_tolerance": - val = FloatPrompt.ask( - f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)", - default=0.05, - ) - if val < 0 or val > 1: - print_error("Slippage tolerance must be between 0 and 1.") - raise typer.Exit() - self.config[arg] = val - + elif arg == "rate_tolerance": + while True: + val = FloatPrompt.ask( + f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)", + default=0.05, + ) + try: + validated_val = validate_rate_tolerance(val) + self.config[arg] = validated_val + break + except typer.BadParameter as e: + print_error(str(e)) + continue else: val = Prompt.ask( f"What value would you like to assign to [red]{arg}[/red]?" @@ -1206,7 +1211,7 @@ def del_config( wallet_hotkey: bool = typer.Option(False, *Options.wallet_hotkey.param_decls), network: bool = typer.Option(False, *Options.network.param_decls), use_cache: bool = typer.Option(False, "--cache"), - slippage_tolerance: bool = typer.Option( + rate_tolerance: bool = typer.Option( False, "--slippage", "--slippage-tolerance", "--tolerance" ), safe_staking: bool = typer.Option( @@ -1247,7 +1252,7 @@ def del_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, - "slippage_tolerance": slippage_tolerance, + "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, } @@ -1311,8 +1316,8 @@ def get_config(self): else: if value in Constants.networks: value = value + f" ({Constants.network_map[value]})" - if key == "slippage_tolerance": - value = f"{value} ({value*100}%)" + if key == "rate_tolerance": + value = f"{value} ({value*100}%)" if value is not None else "None" elif key in deprecated_configs: continue @@ -1326,35 +1331,35 @@ def get_config(self): console.print(table) - def ask_slippage( + def ask_rate_tolerance( self, - slippage_tolerance: Optional[float], + rate_tolerance: Optional[float], ) -> float: """ - Gets slippage tolerance from args, config, or default. + Gets rate tolerance from args, config, or default. Args: - slippage_tolerance (Optional[float]): Explicitly provided slippage value + rate_tolerance (Optional[float]): Explicitly provided slippage value Returns: - float: Slippage tolerance value + float: rate tolerance value """ - if slippage_tolerance is not None: + if rate_tolerance is not None: console.print( - f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{slippage_tolerance} ({slippage_tolerance*100}%)[/bold cyan]." + f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{rate_tolerance} ({rate_tolerance*100}%)[/bold cyan]." ) - return slippage_tolerance - elif self.config.get("slippage_tolerance") is not None: - config_slippage = self.config["slippage_tolerance"] + return rate_tolerance + elif self.config.get("rate_tolerance") is not None: + config_slippage = self.config["rate_tolerance"] console.print( - f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{config_slippage} ({config_slippage*100}%)[/bold cyan] (from config)." + f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{config_slippage} ({config_slippage*100}%)[/bold cyan] (from config)." ) return config_slippage else: console.print( - f"[dim][blue]Slippage tolerance[/blue] is: [bold cyan]{defaults.slippage_tolerance} ({defaults.slippage_tolerance*100}%)[/bold cyan] by default." + f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{defaults.rate_tolerance} ({defaults.rate_tolerance*100}%)[/bold cyan] by default. You can set this using `btcli config set`" ) - return defaults.slippage_tolerance + return defaults.rate_tolerance def ask_safe_staking( self, @@ -1371,19 +1376,19 @@ def ask_safe_staking( """ if safe_staking is not None: console.print( - f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan]." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan]." ) return safe_staking elif self.config.get("safe_staking") is not None: safe_staking = self.config["safe_staking"] console.print( - f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] (from config)." ) return safe_staking else: safe_staking = True console.print( - f"[dim][blue]Safe staking[/blue] is: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] by default." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] by default. You can set this using `btcli config set`" ) return safe_staking @@ -1402,18 +1407,18 @@ def ask_partial_stake( """ if allow_partial_stake is not None: console.print( - f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan]." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan]." ) return allow_partial_stake elif self.config.get("allow_partial_stake") is not None: config_partial = self.config["allow_partial_stake"] console.print( - f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if config_partial else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if config_partial else 'disabled'}[/bold cyan] (from config)." ) return config_partial else: console.print( - f"[dim][blue]Partial staking[/blue] is: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] by default." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] by default. You can set this using `btcli config set`" ) return False @@ -2789,7 +2794,7 @@ def stake_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, - slippage_tolerance: Optional[float] = Options.slippage_tolerance, + rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, prompt: bool = Options.prompt, @@ -2810,8 +2815,8 @@ def stake_add( self.verbosity_handler(quiet, verbose) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: + rate_tolerance = self.ask_rate_tolerance(rate_tolerance) allow_partial_stake = self.ask_partial_stake(allow_partial_stake) - slippage_tolerance = self.ask_slippage(slippage_tolerance) netuid = get_optional_netuid(netuid, all_netuids) if stake_all and amount: @@ -2961,7 +2966,7 @@ def stake_add( included_hotkeys, excluded_hotkeys, safe_staking, - slippage_tolerance, + rate_tolerance, allow_partial_stake, ) ) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 821fe5e0..af0786d3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -65,7 +65,7 @@ def decode(key: str, default=""): class Defaults: netuid = 1 - slippage_tolerance = 0.005 + rate_tolerance = 0.005 class config: base_path = "~/.bittensor" diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 09c79d9b..65724422 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1227,16 +1227,16 @@ def is_linux(): """Returns True if the operating system is Linux.""" return platform.system().lower() == "linux" -def validate_slippage_tolerance(value: Optional[float]) -> Optional[float]: - """Validates slippage tolerance input""" +def validate_rate_tolerance(value: Optional[float]) -> Optional[float]: + """Validates rate tolerance input""" if value is not None: if value < 0: - raise typer.BadParameter("Slippage tolerance cannot be negative (less than 0%).") + raise typer.BadParameter("Rate tolerance cannot be negative (less than 0%).") if value > 1: - raise typer.BadParameter("Slippage tolerance cannot be greater than 1 (100%).") + raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).") if value > 0.5: console.print( - f"[yellow]Warning: High slippage tolerance of {value*100}% specified. " + f"[yellow]Warning: High rate tolerance of {value*100}% specified. " "This may result in unfavorable transaction execution.[/yellow]" ) return value diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 910c3915..4d3c8650 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + # Helper functions def prompt_stake_amount( current_balance: Balance, netuid: int, action_name: str @@ -126,7 +127,7 @@ def define_stake_table( wallet: Wallet, subtensor: "SubtensorInterface", safe_staking: bool, - slippage_tolerance: float, + rate_tolerance: float, ) -> Table: """Creates and initializes a table for displaying stake information. @@ -177,7 +178,7 @@ def define_stake_table( if safe_staking: table.add_column( - f"Rate with tolerance: [blue]({slippage_tolerance*100}%)[/blue]", + f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]", justify="center", style=COLOR_PALETTE["POOLS"]["RATE"], ) @@ -242,11 +243,13 @@ def calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, floa ) if subnet_info.is_dynamic: slippage_str = f"{slippage_pct_float:.4f} %" - rate = str(1 / (float(subnet_info.price) or 1)) + rate = f"{(1 / subnet_info.price.tao or 1):.4f}" + print(f"rate: {rate}") else: slippage_pct_float = 0 slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" - rate = str(1) + rate = "1" + print(f"rate: {rate}") return received_amount, slippage_str, slippage_pct_float, rate @@ -263,7 +266,7 @@ async def stake_add( include_hotkeys: list[str], exclude_hotkeys: list[str], safe_staking: bool, - slippage_tolerance: float, + rate_tolerance: float, allow_partial_stake: bool, ): """ @@ -280,12 +283,13 @@ async def stake_add( include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) safe_staking: whether to use safe staking - slippage_tolerance: slippage tolerance percentage for stake operations + rate_tolerance: rate tolerance percentage for stake operations allow_partial_stake: whether to allow partial stake Returns: bool: True if stake operation is successful, False otherwise """ + async def safe_stake_extrinsic( netuid: int, amount: Balance, @@ -294,13 +298,13 @@ async def safe_stake_extrinsic( price_limit: Balance, wallet: Wallet, subtensor: "SubtensorInterface", - status=None + status=None, ) -> None: err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid}" ) - + current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) next_nonce = await subtensor.substrate.get_account_next_index( wallet.coldkeypub.ss58_address ) @@ -323,9 +327,18 @@ async def safe_stake_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) except SubstrateRequestException as e: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" - ) + if "Custom error: 8" in str(e): + print_error( + f"\n{failure_prelude}: Price exceeded tolerance limit. " + f"Transaction rejected because partial staking is disabled. " + f"Either increase price tolerance or enable partial staking.", + status=status, + ) + return + else: + err_out( + f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + ) return else: await response.process_events() @@ -334,22 +347,24 @@ async def safe_stake_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: + block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=netuid, + block_hash=block_hash, ), ) console.print( f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid}[/dark_sea_green3]" ) console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) - amount_staked = current_wallet_balance - new_balance + amount_staked = current_balance - new_balance if allow_partial_stake and (amount_staked != amount): console.print( "Partial stake transaction. Staked:\n" @@ -370,13 +385,13 @@ async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None ): err_out = partial(print_error, status=status) + current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) next_nonce = await subtensor.substrate.get_account_next_index( wallet.coldkeypub.ss58_address ) - call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -417,7 +432,7 @@ async def stake_extrinsic( f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" ) console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " @@ -526,17 +541,21 @@ async def stake_extrinsic( if safe_staking: if subnet_info.is_dynamic: rate = 1 / subnet_info.price.tao or 1 - rate_with_tolerance = rate * (1 + slippage_tolerance) # Rate only for display - price_with_tolerance = subnet_info.price.rao * (1 + slippage_tolerance) # Actual price to pass to extrinsic + _rate_with_tolerance = rate * ( + 1 + rate_tolerance + ) # Rate only for display + rate_with_tolerance = f"{_rate_with_tolerance:.4f}" + price_with_tolerance = subnet_info.price.rao * ( + 1 + rate_tolerance + ) # Actual price to pass to extrinsic else: - rate_with_tolerance = 1 + rate_with_tolerance = "1" price_with_tolerance = Balance.from_rao(1) prices_with_tolerance.append(price_with_tolerance) base_row.extend( [ - str(rate_with_tolerance) - + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # safe staking ] ) @@ -544,9 +563,7 @@ async def stake_extrinsic( rows.append(tuple(base_row)) # Define and print stake table + slippage warning - table = define_stake_table( - wallet, subtensor, safe_staking, slippage_tolerance - ) + table = define_stake_table(wallet, subtensor, safe_staking, rate_tolerance) for row in rows: table.add_row(*row) print_table_and_slippage(table, max_slippage, safe_staking) @@ -563,7 +580,9 @@ async def stake_extrinsic( if safe_staking: stake_coroutines = [] for i, (ni, am, curr, price_with_tolerance) in enumerate( - zip(netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance) + zip( + netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance + ) ): for _, staking_address in hotkeys_to_stake_to: # Regular extrinsic for root subnet @@ -601,7 +620,8 @@ async def stake_extrinsic( ) for _, staking_address in hotkeys_to_stake_to ] - + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): - await asyncio.gather(*stake_coroutines) - \ No newline at end of file + # We can gather them all at once but balance reporting will be in race-condition. + for coroutine in stake_coroutines: + await coroutine From 8f5d55f0e3c26adb9e1a9d9af468b3bba9bd1726 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Feb 2025 22:48:53 +0200 Subject: [PATCH 300/332] Missed `.run` --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dbe6cdf1..265a0b1f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1007,7 +1007,7 @@ def main_callback( uvloop.install() self.asyncio_runner = asyncio.run except ModuleNotFoundError: - self.asyncio_runner = asyncio + self.asyncio_runner = asyncio.run def verbosity_handler(self, quiet: bool, verbose: bool): if quiet and verbose: From 33880d423b52585acf0130c692d14daa53add09e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 17:13:49 -0800 Subject: [PATCH 301/332] Adds stake.remove - working --- bittensor_cli/cli.py | 52 +- bittensor_cli/src/commands/stake/add.py | 18 +- bittensor_cli/src/commands/stake/remove.py | 994 +++++++++++++++++++++ bittensor_cli/src/commands/stake/stake.py | 806 +---------------- 4 files changed, 1030 insertions(+), 840 deletions(-) create mode 100644 bittensor_cli/src/commands/stake/remove.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 585d1288..df427caa 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -37,7 +37,8 @@ children_hotkeys, stake, move, - add as stake_add, + add as add_stake, + remove as remove_stake, ) from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters @@ -1038,7 +1039,7 @@ def main_callback( uvloop.install() self.asyncio_runner = asyncio.run except ModuleNotFoundError: - self.asyncio_runner = asyncio + self.asyncio_runner = asyncio.run def verbosity_handler(self, quiet: bool, verbose: bool): if quiet and verbose: @@ -2975,7 +2976,7 @@ def stake_add( raise typer.Exit() return self._run_command( - stake_add.stake_add( + add_stake.stake_add( wallet, self.initialize_chain(network), netuid, @@ -3020,12 +3021,6 @@ def stake_remove( "", help="The ss58 address of the hotkey to unstake from.", ), - keep_stake: float = typer.Option( - 0.0, - "--keep-stake", - "--keep", - help="Sets the maximum amount of TAO to remain staked in each hotkey.", - ), include_hotkeys: str = typer.Option( "", "--include-hotkeys", @@ -3185,23 +3180,30 @@ def stake_remove( else: excluded_hotkeys = [] - return self._run_command( - stake.unstake( - wallet, - self.initialize_chain(network), - hotkey_ss58_address, - all_hotkeys, - included_hotkeys, - excluded_hotkeys, - amount, - keep_stake, - unstake_all, - prompt, - interactive, - netuid=netuid, - unstake_all_alpha=unstake_all_alpha, + if unstake_all or unstake_all_alpha: + return self._run_command( + remove_stake.unstake_all( + wallet=wallet, + subtensor=self.initialize_chain(network), + unstake_all_alpha=unstake_all_alpha, + prompt=prompt, + ) + ) + else: + return self._run_command( + remove_stake.unstake( + wallet=wallet, + subtensor=self.initialize_chain(network), + hotkey_ss58_address=hotkey_ss58_address, + all_hotkeys=all_hotkeys, + include_hotkeys=included_hotkeys, + exclude_hotkeys=excluded_hotkeys, + amount=amount, + prompt=prompt, + interactive=interactive, + netuid=netuid, + ) ) - ) def stake_move( self, diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 4d3c8650..f2067cbb 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -26,7 +26,7 @@ # Helper functions -def prompt_stake_amount( +def _prompt_stake_amount( current_balance: Balance, netuid: int, action_name: str ) -> tuple[Balance, bool]: """Prompts user to input a stake amount with validation. @@ -123,7 +123,7 @@ def _get_hotkeys_to_stake_to( return [(None, wallet.hotkey.ss58_address)] -def define_stake_table( +def _define_stake_table( wallet: Wallet, subtensor: "SubtensorInterface", safe_staking: bool, @@ -190,7 +190,7 @@ def define_stake_table( return table -def print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool): +def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool): """Prints the stake table, slippage warning, and table description. Args: @@ -225,7 +225,7 @@ def print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bo console.print(base_description + (safe_staking_description if safe_staking else "")) -def calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: +def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: """Calculate slippage when adding stake. Args: @@ -244,12 +244,10 @@ def calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, floa if subnet_info.is_dynamic: slippage_str = f"{slippage_pct_float:.4f} %" rate = f"{(1 / subnet_info.price.tao or 1):.4f}" - print(f"rate: {rate}") else: slippage_pct_float = 0 slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" rate = "1" - print(f"rate: {rate}") return received_amount, slippage_str, slippage_pct_float, rate @@ -504,7 +502,7 @@ async def stake_extrinsic( elif stake_all: amount_to_stake = current_wallet_balance / len(netuids) elif not amount: - amount_to_stake, _ = prompt_stake_amount( + amount_to_stake, _ = _prompt_stake_amount( current_balance=remaining_wallet_balance, netuid=netuid, action_name="stake", @@ -522,7 +520,7 @@ async def stake_extrinsic( # Calculate slippage received_amount, slippage_pct, slippage_pct_float, rate = ( - calculate_slippage(subnet_info, amount_to_stake) + _calculate_slippage(subnet_info, amount_to_stake) ) max_slippage = max(slippage_pct_float, max_slippage) @@ -563,10 +561,10 @@ async def stake_extrinsic( rows.append(tuple(base_row)) # Define and print stake table + slippage warning - table = define_stake_table(wallet, subtensor, safe_staking, rate_tolerance) + table = _define_stake_table(wallet, subtensor, safe_staking, rate_tolerance) for row in rows: table.add_row(*row) - print_table_and_slippage(table, max_slippage, safe_staking) + _print_table_and_slippage(table, max_slippage, safe_staking) if prompt: if not Confirm.ask("Would you like to continue?"): diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py new file mode 100644 index 00000000..8dd49ad6 --- /dev/null +++ b/bittensor_cli/src/commands/stake/remove.py @@ -0,0 +1,994 @@ +import asyncio +from functools import partial + +from typing import TYPE_CHECKING, Optional +import typer + +from bittensor_wallet import Wallet +from bittensor_wallet.errors import KeyFileError +from rich.prompt import Confirm, Prompt +from rich.table import Table + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.utils import ( + # TODO add back in caching + console, + err_console, + print_verbose, + print_error, + get_hotkey_wallets_for_wallet, + is_valid_ss58_address, + format_error_message, + group_subnets, + millify_tao, + get_subnet_name, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +# Commands +async def unstake( + wallet: Wallet, + subtensor: "SubtensorInterface", + hotkey_ss58_address: str, + all_hotkeys: bool, + include_hotkeys: list[str], + exclude_hotkeys: list[str], + amount: float, + prompt: bool, + interactive: bool = False, + netuid: Optional[int] = None, +): + """Unstake from hotkey(s).""" + unstake_all_from_hk = False + with console.status( + f"Retrieving subnet data & identities from {subtensor.network}...", + spinner="earth", + ): + all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.all_subnets(), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + if interactive: + hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection( + subtensor, + wallet, + all_sn_dynamic_info, + ck_hk_identities, + old_identities, + netuid=netuid, + ) + if not hotkeys_to_unstake_from: + console.print("[red]No unstake operations to perform.[/red]") + return False + netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from}) + + else: + netuids = ( + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() + ) + hotkeys_to_unstake_from = _get_hotkeys_to_unstake( + wallet=wallet, + hotkey_ss58_address=hotkey_ss58_address, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, + ) + + with console.status( + f"Retrieving stake data from {subtensor.network}...", + spinner="earth", + ): + # Fetch stake balances + chain_head = await subtensor.substrate.get_chain_head() + stake_info_list = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + block_hash=chain_head, + ) + stake_in_netuids = {} + for stake_info in stake_info_list: + if stake_info.hotkey_ss58 not in stake_in_netuids: + stake_in_netuids[stake_info.hotkey_ss58] = {} + stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) + + # Flag to check if user wants to quit + skip_remaining_subnets = False + if len(netuids) > 1 and not amount: + console.print( + "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" + ) + + # Iterate over hotkeys and netuids to collect unstake operations + unstake_all_hk_ss58 = None + unstake_operations = [] + total_received_amount = Balance.from_tao(0) + max_float_slippage = 0 + for hotkey in hotkeys_to_unstake_from: + if skip_remaining_subnets: + break + + if interactive: + staking_address_name, staking_address_ss58, netuid = hotkey + netuids_to_process = [netuid] + else: + staking_address_name, staking_address_ss58 = hotkey + netuids_to_process = netuids + + initial_amount = amount + + for netuid in netuids_to_process: + if skip_remaining_subnets: + break # Exit the loop over netuids + + subnet_info = all_sn_dynamic_info.get(netuid) + if staking_address_ss58 not in stake_in_netuids: + print_error( + f"No stake found for hotkey: {staking_address_ss58} on netuid: {netuid}" + ) + continue # Skip to next hotkey + + current_stake_balance = stake_in_netuids[staking_address_ss58].get(netuid) + if current_stake_balance is None or current_stake_balance.tao == 0: + print_error( + f"No stake to unstake from {staking_address_ss58} on netuid: {netuid}" + ) + continue # No stake to unstake + + # Determine the amount we are unstaking. + if unstake_all_from_hk: + amount_to_unstake_as_balance = current_stake_balance + unstake_all_hk_ss58 = staking_address_ss58 + elif initial_amount: + amount_to_unstake_as_balance = Balance.from_tao(initial_amount) + else: + amount_to_unstake_as_balance = _ask_unstake_amount( + current_stake_balance, + netuid, + staking_address_name + if staking_address_name + else staking_address_ss58, + staking_address_ss58, + interactive, + ) + if amount_to_unstake_as_balance is None: + skip_remaining_subnets = True + break + + # Check enough stake to remove. + amount_to_unstake_as_balance.set_unit(netuid) + if amount_to_unstake_as_balance > current_stake_balance: + err_console.print( + f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]" + f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange] on netuid: {netuid}" + ) + continue # Skip to the next subnet - useful when single amount is specified for all subnets + + received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( + subnet_info=subnet_info, amount=amount_to_unstake_as_balance + ) + total_received_amount += received_amount + max_float_slippage = max(max_float_slippage, slippage_pct_float) + + unstake_operations.append( + { + "netuid": netuid, + "hotkey_name": staking_address_name + if staking_address_name + else staking_address_ss58, + "hotkey_ss58": staking_address_ss58, + "amount_to_unstake": amount_to_unstake_as_balance, + "current_stake_balance": current_stake_balance, + "received_amount": received_amount, + "slippage_pct": slippage_pct, + "slippage_pct_float": slippage_pct_float, + "dynamic_info": subnet_info, + } + ) + + if not unstake_operations: + console.print("[red]No unstake operations to perform.[/red]") + return False + + table = _create_unstake_table( + wallet_name=wallet.name, + wallet_coldkey_ss58=wallet.coldkeypub.ss58_address, + network=subtensor.network, + total_received_amount=total_received_amount, + ) + + for op in unstake_operations: + subnet_info = op["dynamic_info"] + table.add_row( + str(op["netuid"]), # Netuid + op["hotkey_name"], # Hotkey Name + str(op["amount_to_unstake"]), # Amount to Unstake + str(float(subnet_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})", # Rate + str(op["received_amount"]), # Received Amount + op["slippage_pct"], # Slippage Percent + ) + + _print_table_and_slippage(table, max_float_slippage) + if prompt: + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Execute extrinsics + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status("\n:satellite: Performing unstaking operations...") as status: + if unstake_all_from_hk: + await _unstake_all_extrinsic( + hotkey_ss58=unstake_all_hk_ss58, + wallet=wallet, + subtensor=subtensor, + status=status, + ) + else: + for op in unstake_operations: + await _unstake_extrinsic( + netuid=op["netuid"], + amount=op["amount_to_unstake"], + current_stake=op["current_stake_balance"], + hotkey_ss58=op["hotkey_ss58"], + wallet=wallet, + subtensor=subtensor, + status=status, + ) + console.print( + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." + ) + + +async def unstake_all( + wallet: Wallet, + subtensor: "SubtensorInterface", + unstake_all_alpha: bool = False, + prompt: bool = True, +) -> bool: + """Unstakes all stakes from all hotkeys in all subnets.""" + + with console.status( + f"Retrieving stake information & identities from {subtensor.network}...", + spinner="earth", + ): + ( + stake_info, + ck_hk_identities, + old_identities, + all_sn_dynamic_info_, + current_wallet_balance, + ) = await asyncio.gather( + subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + subtensor.all_subnets(), + subtensor.get_balance(wallet.coldkeypub.ss58_address), + ) + + if unstake_all_alpha: + stake_info = [stake for stake in stake_info if stake.netuid != 0] + + if not stake_info: + console.print("[red]No stakes found to unstake[/red]") + return False + + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + # Create table for unstaking all + table_title = ( + "Unstaking Summary - All Stakes" + if not unstake_all_alpha + else "Unstaking Summary - All Alpha Stakes" + ) + table = Table( + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + ), + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Current Stake ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + ) + table.add_column( + f"Rate ({Balance.unit}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"Recieved ({Balance.unit})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) + + # Calculate slippage and total received + max_slippage = 0.0 + total_received_value = Balance(0) + for stake in stake_info: + if stake.stake.rao == 0: + continue + + # Get hotkey identity + if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58): + hotkey_name = hk_identity.get("identity", {}).get( + "name", "" + ) or hk_identity.get("display", "~") + hotkey_display = f"{hotkey_name}" + elif old_identity := old_identities.get(stake.hotkey_ss58): + hotkey_name = old_identity.display + hotkey_display = f"{hotkey_name}" + else: + hotkey_display = stake.hotkey_ss58 + + subnet_info = all_sn_dynamic_info.get(stake.netuid) + stake_amount = stake.stake + received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( + subnet_info=subnet_info, amount=stake_amount + ) + max_slippage = max(max_slippage, slippage_pct_float) + total_received_value += received_amount + + table.add_row( + str(stake.netuid), + hotkey_display, + str(stake_amount), + str(float(subnet_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", + str(received_amount), + slippage_pct, + ) + console.print(table) + message = "" + if max_slippage > 5: + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + console.print( + f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" + ) + + if prompt and not Confirm.ask( + "\nDo you want to proceed with unstaking everything?" + ): + return False + + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + console_status = ( + ":satellite: Unstaking all Alpha stakes..." + if unstake_all_alpha + else ":satellite: Unstaking all stakes..." + ) + with console.status(console_status): + call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params={"hotkey": wallet.hotkey.ss58_address}, + ) + success, error_message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + if success: + success_message = ( + ":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]" + if not unstake_all_alpha + else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" + ) + console.print(success_message) + new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + if unstake_all_alpha: + root_stake = await subtensor.get_stake( + hotkey_ss58=wallet.hotkey.ss58_address, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=0, + ) + console.print( + f"Root Stake:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{root_stake}" + ) + return True + else: + err_console.print( + f":cross_mark: [red]Failed to unstake[/red]: {error_message}" + ) + return False + + +# Extrinsics +async def _unstake_extrinsic( + netuid: int, + amount: Balance, + current_stake: Balance, + hotkey_ss58: str, + wallet: Wallet, + subtensor: "SubtensorInterface", + status=None, +) -> None: + """Execute a standard unstake extrinsic. + + Args: + netuid: The subnet ID + amount: Amount to unstake + current_stake: Current stake balance + hotkey_ss58: Hotkey SS58 address + wallet: Wallet instance + subtensor: Subtensor interface + status: Optional status for console updates + """ + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" + ) + + if status: + status.update( + f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." + ) + + current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + await response.process_events() + + if not await response.is_success: + err_out( + f"{failure_prelude} with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}" + ) + return + + # Fetch latest balance and stake + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block_hash=block_hash, + ), + ) + + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + + except Exception as e: + err_out(f"{failure_prelude} with error: {str(e)}") + + +async def _unstake_all_extrinsic( + hotkey_ss58: str, + wallet: Wallet, + subtensor: "SubtensorInterface", + status=None, +) -> None: + """Execute an unstake_all extrinsic. + + Args: + hotkey_ss58: Hotkey SS58 address + wallet: Wallet instance + subtensor: Subtensor interface + status: Optional status for console updates + """ + current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="unstake_all", + call_params={"hotkey": hotkey_ss58}, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + await response.process_events() + + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) + return + + new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + + except Exception as e: + print_error(f":cross_mark: [red]Failed[/red] with error: {str(e)}", status) + + +# Helpers +def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: + """Calculate slippage and received amount for unstaking operation. + + Args: + dynamic_info: Subnet information containing price data + amount: Amount being unstaked + + Returns: + tuple containing: + - received_amount: Balance after slippage + - slippage_pct: Formatted string of slippage percentage + - slippage_pct_float: Float value of slippage percentage + """ + received_amount, _, slippage_pct_float = subnet_info.alpha_to_tao_with_slippage( + amount + ) + + if subnet_info.is_dynamic: + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "[red]N/A[/red]" + + return received_amount, slippage_pct, slippage_pct_float + + +async def _unstake_selection( + subtensor: "SubtensorInterface", + wallet: Wallet, + dynamic_info, + identities, + old_identities, + netuid: Optional[int] = None, +): + stake_infos = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + + if not stake_infos: + print_error("You have no stakes to unstake.") + raise typer.Exit() + + hotkey_stakes = {} + for stake_info in stake_infos: + if netuid is not None and stake_info.netuid != netuid: + continue + hotkey_ss58 = stake_info.hotkey_ss58 + netuid_ = stake_info.netuid + stake_amount = stake_info.stake + if stake_amount.tao > 0: + hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount + + if not hotkey_stakes: + if netuid is not None: + print_error(f"You have no stakes to unstake in subnet {netuid}.") + else: + print_error("You have no stakes to unstake.") + raise typer.Exit() + + hotkeys_info = [] + for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): + if hk_identity := identities["hotkeys"].get(hotkey_ss58): + hotkey_name = hk_identity.get("identity", {}).get( + "name", "" + ) or hk_identity.get("display", "~") + elif old_identity := old_identities.get(hotkey_ss58): + hotkey_name = old_identity.display + else: + hotkey_name = "~" + # TODO: Add wallet ids here. + + hotkeys_info.append( + { + "index": idx, + "identity": hotkey_name, + "netuids": list(netuid_stakes.keys()), + "hotkey_ss58": hotkey_ss58, + } + ) + + # Display existing hotkeys, id, and staked netuids. + subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Index", justify="right") + table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + + for hotkey_info in hotkeys_info: + index = str(hotkey_info["index"]) + identity = hotkey_info["identity"] + netuids = group_subnets([n for n in hotkey_info["netuids"]]) + hotkey_ss58 = hotkey_info["hotkey_ss58"] + table.add_row(index, identity, netuids, hotkey_ss58) + + console.print("\n", table) + + # Prompt to select hotkey to unstake. + hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info] + hotkey_idx = Prompt.ask( + "\nEnter the index of the hotkey you want to unstake from", + choices=hotkey_options, + ) + selected_hotkey_info = hotkeys_info[int(hotkey_idx)] + selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"] + selected_hotkey_name = selected_hotkey_info["identity"] + netuid_stakes = hotkey_stakes[selected_hotkey_ss58] + + # Display hotkey's staked netuids with amount. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + table.add_column("Subnet", justify="right") + table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column( + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style=COLOR_PALETTE["POOLS"]["RATE"], + justify="left", + ) + + for netuid_, stake_amount in netuid_stakes.items(): + symbol = dynamic_info[netuid_].symbol + rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" + table.add_row(str(netuid_), symbol, str(stake_amount), rate) + console.print("\n", table, "\n") + + # Ask which netuids to unstake from for the selected hotkey. + unstake_all = False + if netuid is not None: + selected_netuids = [netuid] + else: + while True: + netuid_input = Prompt.ask( + "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", + default="all", + ) + + if netuid_input.lower() == "all": + selected_netuids = list(netuid_stakes.keys()) + unstake_all = True + break + else: + try: + netuid_list = [int(n.strip()) for n in netuid_input.split(",")] + invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] + if invalid_netuids: + print_error( + f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + ) + else: + selected_netuids = netuid_list + break + except ValueError: + print_error( + "Please enter valid netuids (numbers), separated by commas, or 'all'." + ) + + hotkeys_to_unstake_from = [] + for netuid_ in selected_netuids: + hotkeys_to_unstake_from.append( + (selected_hotkey_name, selected_hotkey_ss58, netuid_) + ) + return hotkeys_to_unstake_from, unstake_all + + +def _ask_unstake_amount( + current_stake_balance: Balance, + netuid: int, + staking_address_name: str, + staking_address_ss58: str, + interactive: bool, +) -> Optional[Balance]: + """Prompt the user to decide the amount to unstake. + + Args: + current_stake_balance: The current stake balance available to unstake + netuid: The subnet ID + staking_address_name: Display name of the staking address + staking_address_ss58: SS58 address of the staking address + interactive: Whether in interactive mode (affects default choice) + + Returns: + Balance amount to unstake, or None if user chooses to quit + """ + stake_color = COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"] + display_address = ( + staking_address_name if staking_address_name else staking_address_ss58 + ) + + # First prompt: Ask if user wants to unstake all + unstake_all_prompt = ( + f"Unstake all: [{stake_color}]{current_stake_balance}[/{stake_color}]" + f" from [{stake_color}]{display_address}[/{stake_color}]" + f" on netuid: [{stake_color}]{netuid}[/{stake_color}]? [y/n/q]" + ) + + while True: + response = Prompt.ask( + unstake_all_prompt, + choices=["y", "n", "q"], + default="n", + show_choices=True, + ).lower() + + if response == "q": + return None + if response == "y": + return current_stake_balance + if response != "n": + console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]") + continue + + amount_prompt = ( + f"Enter amount to unstake in [{stake_color}]{Balance.get_unit(netuid)}[/{stake_color}]" + f" from subnet: [{stake_color}]{netuid}[/{stake_color}]" + f" (Max: [{stake_color}]{current_stake_balance}[/{stake_color}])" + ) + + while True: + amount_input = Prompt.ask(amount_prompt) + if amount_input.lower() == "q": + return None + + try: + amount_value = float(amount_input) + + # Validate amount + if amount_value <= 0: + console.print("[red]Amount must be greater than zero.[/red]") + continue + + amount_to_unstake = Balance.from_tao(amount_value) + amount_to_unstake.set_unit(netuid) + + if amount_to_unstake > current_stake_balance: + console.print( + f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" + ) + continue + + return amount_to_unstake + + except ValueError: + console.print( + "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" + ) + + +def _get_hotkeys_to_unstake( + wallet: Wallet, + hotkey_ss58_address: Optional[str], + all_hotkeys: bool, + include_hotkeys: list[str], + exclude_hotkeys: list[str], +) -> list[tuple[Optional[str], str]]: + """Get list of hotkeys to unstake from based on input parameters. + + Args: + wallet: The wallet to unstake from + hotkey_ss58_address: Specific hotkey SS58 address to unstake from + all_hotkeys: Whether to unstake from all hotkeys + include_hotkeys: List of hotkey names/addresses to include + exclude_hotkeys: List of hotkey names to exclude + + Returns: + List of tuples containing (hotkey_name, hotkey_ss58) pairs to unstake from + """ + if hotkey_ss58_address: + print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") + return [(None, hotkey_ss58_address)] + + if all_hotkeys: + print_verbose("Unstaking from all hotkeys") + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + return [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in exclude_hotkeys + ] + + if include_hotkeys: + print_verbose("Unstaking from included hotkeys") + result = [] + for hotkey_identifier in include_hotkeys: + if is_valid_ss58_address(hotkey_identifier): + result.append((None, hotkey_identifier)) + else: + wallet_ = Wallet( + name=wallet.name, + path=wallet.path, + hotkey=hotkey_identifier, + ) + result.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) + return result + + # Only cli.config.wallet.hotkey is specified + print_verbose( + f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" + ) + assert wallet.hotkey is not None + return [(wallet.hotkey_str, wallet.hotkey.ss58_address)] + + +def _create_unstake_table( + wallet_name: str, + wallet_coldkey_ss58: str, + network: str, + total_received_amount: Balance, +) -> Table: + """Create a table summarizing unstake operations. + + Args: + wallet_name: Name of the wallet + wallet_coldkey_ss58: Coldkey SS58 address + network: Network name + total_received_amount: Total amount to be received after unstaking + + Returns: + Rich Table object configured for unstake summary + """ + title = ( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + ) + table = Table( + title=title, + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"Received ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + footer=str(total_received_amount), + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + + return table + + +def _print_table_and_slippage( + table: Table, + max_float_slippage: float, +) -> None: + """Print the unstake summary table and additional information. + + Args: + table: The Rich table containing unstake details + max_float_slippage: Maximum slippage percentage across all operations + """ + console.print(table) + + if max_float_slippage > 5: + console.print( + "\n" + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," + " this may result in a loss of funds.\n" + f"-------------------------------------------------------------------------------------------------------------------\n" + ) + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake remove operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. + - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. + - [bold white]Amount[/bold white]: The stake amount you are removing from this key. + - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b94b08ed..fb8dfed1 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -4,8 +4,7 @@ import typer from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm, Prompt +from rich.prompt import Prompt from rich.table import Table from rich import box from rich.progress import Progress, BarColumn, TextColumn @@ -18,13 +17,7 @@ from bittensor_cli.src.bittensor.utils import ( # TODO add back in caching console, - err_console, - print_verbose, print_error, - get_hotkey_wallets_for_wallet, - is_valid_ss58_address, - format_error_message, - group_subnets, millify_tao, get_subnet_name, ) @@ -33,803 +26,6 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def unstake_selection( - subtensor: "SubtensorInterface", - wallet: Wallet, - dynamic_info, - identities, - old_identities, - netuid: Optional[int] = None, -): - stake_infos = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - - if not stake_infos: - print_error("You have no stakes to unstake.") - raise typer.Exit() - - hotkey_stakes = {} - for stake_info in stake_infos: - if netuid is not None and stake_info.netuid != netuid: - continue - hotkey_ss58 = stake_info.hotkey_ss58 - netuid_ = stake_info.netuid - stake_amount = stake_info.stake - if stake_amount.tao > 0: - hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount - - if not hotkey_stakes: - if netuid is not None: - print_error(f"You have no stakes to unstake in subnet {netuid}.") - else: - print_error("You have no stakes to unstake.") - raise typer.Exit() - - hotkeys_info = [] - for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): - if hk_identity := identities["hotkeys"].get(hotkey_ss58): - hotkey_name = hk_identity.get("identity", {}).get( - "name", "" - ) or hk_identity.get("display", "~") - elif old_identity := old_identities.get(hotkey_ss58): - hotkey_name = old_identity.display - else: - hotkey_name = "~" - # TODO: Add wallet ids here. - - hotkeys_info.append( - { - "index": idx, - "identity": hotkey_name, - "netuids": list(netuid_stakes.keys()), - "hotkey_ss58": hotkey_ss58, - } - ) - - # Display existing hotkeys, id, and staked netuids. - subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column("Index", justify="right") - table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) - table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) - table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) - - for hotkey_info in hotkeys_info: - index = str(hotkey_info["index"]) - identity = hotkey_info["identity"] - netuids = group_subnets([n for n in hotkey_info["netuids"]]) - hotkey_ss58 = hotkey_info["hotkey_ss58"] - table.add_row(index, identity, netuids, hotkey_ss58) - - console.print("\n", table) - - # Prompt to select hotkey to unstake. - hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info] - hotkey_idx = Prompt.ask( - "\nEnter the index of the hotkey you want to unstake from", - choices=hotkey_options, - ) - selected_hotkey_info = hotkeys_info[int(hotkey_idx)] - selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"] - selected_hotkey_name = selected_hotkey_info["identity"] - netuid_stakes = hotkey_stakes[selected_hotkey_ss58] - - # Display hotkey's staked netuids with amount. - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column("Subnet", justify="right") - table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) - table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) - table.add_column( - f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", - style=COLOR_PALETTE["POOLS"]["RATE"], - justify="left", - ) - - for netuid_, stake_amount in netuid_stakes.items(): - symbol = dynamic_info[netuid_].symbol - rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" - table.add_row(str(netuid_), symbol, str(stake_amount), rate) - console.print("\n", table, "\n") - - # Ask which netuids to unstake from for the selected hotkey. - unstake_all = False - if netuid is not None: - selected_netuids = [netuid] - else: - while True: - netuid_input = Prompt.ask( - "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", - default="all", - ) - - if netuid_input.lower() == "all": - selected_netuids = list(netuid_stakes.keys()) - unstake_all = True - break - else: - try: - netuid_list = [int(n.strip()) for n in netuid_input.split(",")] - invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] - if invalid_netuids: - print_error( - f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." - ) - else: - selected_netuids = netuid_list - break - except ValueError: - print_error( - "Please enter valid netuids (numbers), separated by commas, or 'all'." - ) - - hotkeys_to_unstake_from = [] - for netuid_ in selected_netuids: - hotkeys_to_unstake_from.append( - (selected_hotkey_name, selected_hotkey_ss58, netuid_) - ) - return hotkeys_to_unstake_from, unstake_all - - -def ask_unstake_amount( - current_stake_balance: Balance, - netuid: int, - staking_address_name: str, - staking_address_ss58: str, - interactive: bool, -) -> Optional[Balance]: - """Prompt the user to decide the amount to unstake.""" - while True: - response = Prompt.ask( - f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]", - choices=["y", "n", "q"], - default="n" if interactive else "y", - show_choices=True, - ).lower() - - if response == "q": - return None # Quit - - elif response == "y": - return current_stake_balance - - elif response == "n": - while True: - amount_input = Prompt.ask( - f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])" - ) - if amount_input.lower() == "q": - return None # Quit - - try: - amount_value = float(amount_input) - if amount_value <= 0: - console.print("[red]Amount must be greater than zero.[/red]") - continue # Re-prompt - - amount_to_unstake = Balance.from_tao(amount_value) - amount_to_unstake.set_unit(netuid) - if amount_to_unstake > current_stake_balance: - console.print( - f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" - ) - continue # Re-prompt - - return amount_to_unstake - - except ValueError: - console.print( - "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" - ) - - else: - console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]") - - -async def _unstake_all( - wallet: Wallet, - subtensor: "SubtensorInterface", - unstake_all_alpha: bool = False, - prompt: bool = True, -) -> bool: - """Unstakes all stakes from all hotkeys in all subnets.""" - - with console.status( - f"Retrieving stake information & identities from {subtensor.network}...", - spinner="earth", - ): - ( - stake_info, - ck_hk_identities, - old_identities, - all_sn_dynamic_info_, - current_wallet_balance, - ) = await asyncio.gather( - subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address), - subtensor.fetch_coldkey_hotkey_identities(), - subtensor.get_delegate_identities(), - subtensor.all_subnets(), - subtensor.get_balance(wallet.coldkeypub.ss58_address), - ) - - if unstake_all_alpha: - stake_info = [stake for stake in stake_info if stake.netuid != 0] - - if not stake_info: - console.print("[red]No stakes found to unstake[/red]") - return False - - all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} - - # Calculate total value and slippage for all stakes - total_received_value = Balance(0) - table_title = ( - "Unstaking Summary - All Stakes" - if not unstake_all_alpha - else "Unstaking Summary - All Alpha Stakes" - ) - table = Table( - title=( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" - ), - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Current Stake ({Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], - ) - table.add_column( - f"Rate ({Balance.unit}/{Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - f"Recieved ({Balance.unit})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - ) - table.add_column( - "Slippage", - justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - ) - max_slippage = 0.0 - for stake in stake_info: - if stake.stake.rao == 0: - continue - - dynamic_info = all_sn_dynamic_info.get(stake.netuid) - stake_amount = stake.stake - received_amount, _, slippage_pct_float = ( - dynamic_info.alpha_to_tao_with_slippage(stake_amount) - ) - - total_received_value += received_amount - - # Get hotkey identity - if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58): - hotkey_name = hk_identity.get("identity", {}).get( - "name", "" - ) or hk_identity.get("display", "~") - hotkey_display = f"{hotkey_name}" - elif old_identity := old_identities.get(stake.hotkey_ss58): - hotkey_name = old_identity.display - hotkey_display = f"{hotkey_name}" - else: - hotkey_display = stake.hotkey_ss58 - - if dynamic_info.is_dynamic: - slippage_pct = f"{slippage_pct_float:.4f} %" - else: - slippage_pct_float = 0 - slippage_pct = "[red]N/A[/red]" - - max_slippage = max(max_slippage, slippage_pct_float) - - table.add_row( - str(stake.netuid), - hotkey_display, - str(stake_amount), - str(float(dynamic_info.price)) - + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", - str(received_amount), - slippage_pct, - ) - console.print(table) - message = "" - if max_slippage > 5: - message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - - console.print( - f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" - ) - - if prompt and not Confirm.ask( - "\nDo you want to proceed with unstaking everything?" - ): - return False - - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - console_status = ( - ":satellite: Unstaking all Alpha stakes..." - if unstake_all_alpha - else ":satellite: Unstaking all stakes..." - ) - with console.status(console_status): - call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - success, error_message = await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - if success: - success_message = ( - ":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]" - if not unstake_all_alpha - else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" - ) - console.print(success_message) - new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - return True - else: - err_console.print( - f":cross_mark: [red]Failed to unstake[/red]: {error_message}" - ) - return False - - -async def unstake( - wallet: Wallet, - subtensor: "SubtensorInterface", - hotkey_ss58_address: str, - all_hotkeys: bool, - include_hotkeys: list[str], - exclude_hotkeys: list[str], - amount: float, - keep_stake: float, - unstake_all: bool, - prompt: bool, - interactive: bool = False, - netuid: Optional[int] = None, - unstake_all_alpha: bool = False, -): - """Unstake tokens from hotkey(s).""" - - if unstake_all or unstake_all_alpha: - return await _unstake_all(wallet, subtensor, unstake_all_alpha, prompt) - - unstake_all_from_hk = False - with console.status( - f"Retrieving subnet data & identities from {subtensor.network}...", - spinner="earth", - ): - all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( - subtensor.all_subnets(), - subtensor.fetch_coldkey_hotkey_identities(), - subtensor.get_delegate_identities(), - ) - all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} - - if interactive: - hotkeys_to_unstake_from, unstake_all_from_hk = await unstake_selection( - subtensor, - wallet, - all_sn_dynamic_info, - ck_hk_identities, - old_identities, - netuid=netuid, - ) - if not hotkeys_to_unstake_from: - console.print("[red]No unstake operations to perform.[/red]") - return False - netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from}) - - else: - netuids = ( - [int(netuid)] - if netuid is not None - else await subtensor.get_all_subnet_netuids() - ) - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] - if hotkey_ss58_address: - print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") - # Unstake from specific hotkey. - hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] - elif all_hotkeys: - print_verbose("Unstaking from all hotkeys") - # Unstake from all hotkeys. - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - # Exclude hotkeys that are specified. - hotkeys_to_unstake_from = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in exclude_hotkeys - ] - elif include_hotkeys: - print_verbose("Unstaking from included hotkeys") - # Unstake from specific hotkeys. - for hotkey_identifier in include_hotkeys: - if is_valid_ss58_address(hotkey_identifier): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_unstake_from.append((None, hotkey_identifier)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - name=wallet.name, - path=wallet.path, - hotkey=hotkey_identifier, - ) - hotkeys_to_unstake_from.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - else: - # Only cli.config.wallet.hotkey is specified. - # So we unstake from that single hotkey. - print_verbose( - f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" - ) - assert wallet.hotkey is not None - hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] - - with console.status( - f"Retrieving stake data from {subtensor.network}...", - spinner="earth", - ): - # Prepare unstaking transactions - unstake_operations = [] - total_received_amount = Balance.from_tao(0) - current_wallet_balance: Balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - max_float_slippage = 0 - - # Fetch stake balances - chain_head = await subtensor.substrate.get_chain_head() - stake_info_list = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=chain_head, - ) - stake_in_netuids = {} - for stake_info in stake_info_list: - if stake_info.hotkey_ss58 not in stake_in_netuids: - stake_in_netuids[stake_info.hotkey_ss58] = {} - stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = ( - stake_info.stake - ) - - # Flag to check if user wants to quit - skip_remaining_subnets = False - if hotkeys_to_unstake_from: - console.print( - "[dark_sea_green3]Tip: Enter 'q' any time to skip further entries and process existing unstakes" - ) - - # Iterate over hotkeys and netuids to collect unstake operations - unstake_all_hk_ss58 = None - for hotkey in hotkeys_to_unstake_from: - if skip_remaining_subnets: - break - - if interactive: - staking_address_name, staking_address_ss58, netuid = hotkey - netuids_to_process = [netuid] - else: - staking_address_name, staking_address_ss58 = hotkey - netuids_to_process = netuids - - initial_amount = amount - - if len(netuids_to_process) > 1: - console.print( - "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" - ) - - for netuid in netuids_to_process: - if skip_remaining_subnets: - break # Exit the loop over netuids - - dynamic_info = all_sn_dynamic_info.get(netuid) - current_stake_balance = stake_in_netuids[staking_address_ss58][netuid] - if current_stake_balance.tao == 0: - continue # No stake to unstake - - # Determine the amount we are unstaking. - if unstake_all_from_hk or unstake_all: - amount_to_unstake_as_balance = current_stake_balance - unstake_all_hk_ss58 = staking_address_ss58 - elif initial_amount: - amount_to_unstake_as_balance = Balance.from_tao(initial_amount) - else: - amount_to_unstake_as_balance = ask_unstake_amount( - current_stake_balance, - netuid, - staking_address_name - if staking_address_name - else staking_address_ss58, - staking_address_ss58, - interactive, - ) - if amount_to_unstake_as_balance is None: - skip_remaining_subnets = True - break - - # Check enough stake to remove. - amount_to_unstake_as_balance.set_unit(netuid) - if amount_to_unstake_as_balance > current_stake_balance: - err_console.print( - f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]" - f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]" - ) - continue # Skip to the next subnet - useful when single amount is specified for all subnets - - received_amount, _, slippage_pct_float = ( - dynamic_info.alpha_to_tao_with_slippage(amount_to_unstake_as_balance) - ) - total_received_amount += received_amount - if dynamic_info.is_dynamic: - slippage_pct = f"{slippage_pct_float:.4f} %" - else: - slippage_pct_float = 0 - slippage_pct = "[red]N/A[/red]" - max_float_slippage = max(max_float_slippage, slippage_pct_float) - - unstake_operations.append( - { - "netuid": netuid, - "hotkey_name": staking_address_name - if staking_address_name - else staking_address_ss58, - "hotkey_ss58": staking_address_ss58, - "amount_to_unstake": amount_to_unstake_as_balance, - "current_stake_balance": current_stake_balance, - "received_amount": received_amount, - "slippage_pct": slippage_pct, - "slippage_pct_float": slippage_pct_float, - "dynamic_info": dynamic_info, - } - ) - - if not unstake_operations: - console.print("[red]No unstake operations to perform.[/red]") - return False - - # Build the table - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]," - f" Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Amount ({Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO"], - ) - table.add_column( - f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - f"Received ({Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - footer=f"{total_received_amount}", - ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) - - for op in unstake_operations: - dynamic_info = op["dynamic_info"] - table.add_row( - str(op["netuid"]), - op["hotkey_name"], - str(op["amount_to_unstake"]), - str(float(dynamic_info.price)) - + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})", - str(op["received_amount"]), - op["slippage_pct"], - ) - - console.print(table) - - if max_float_slippage > 5: - console.print( - "\n" - f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," - " this may result in a loss of funds.\n" - f"-------------------------------------------------------------------------------------------------------------------\n" - ) - - console.print( - """ -[bold white]Description[/bold white]: -The table displays information about the stake remove operation you are about to perform. -The columns are as follows: - - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. - - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. - - [bold white]Amount[/bold white]: The stake amount you are removing from this key. - - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). -""" - ) - if prompt: - if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() - - # Perform unstaking operations - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - with console.status("\n:satellite: Performing unstaking operations...") as status: - if unstake_all_from_hk: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="unstake_all", - call_params={ - "hotkey": unstake_all_hk_ss58, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - else: - for op in unstake_operations: - netuid_i = op["netuid"] - staking_address_name = op["hotkey_name"] - staking_address_ss58 = op["hotkey_ss58"] - amount = op["amount_to_unstake"] - current_stake_balance = op["current_stake_balance"] - - status.update( - f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..." - ) - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_unstaked": amount.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_stake_info = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - ) - new_stake = Balance.from_rao(0) - for stake_info in new_stake_info: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - console.print( - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." - ) - - async def stake_list( wallet: Wallet, coldkey_ss58: str, From f5a30acc9568f21d13ba32487fe6975919c5c487 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 17:15:15 -0800 Subject: [PATCH 302/332] updates names --- bittensor_cli/cli.py | 12 ++++++------ .../src/commands/stake/{stake.py => list.py} | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename bittensor_cli/src/commands/stake/{stake.py => list.py} (100%) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index df427caa..c030fb32 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -35,8 +35,8 @@ from bittensor_cli.src.commands.subnets import price, subnets from bittensor_cli.src.commands.stake import ( children_hotkeys, - stake, - move, + list as list_stake, + move as move_stake, add as add_stake, remove as remove_stake, ) @@ -2768,7 +2768,7 @@ def stake_list( ) return self._run_command( - stake.stake_list( + list_stake.stake_list( wallet, coldkey_ss58, self.initialize_chain(network), @@ -3344,7 +3344,7 @@ def stake_move( ) return self._run_command( - move.move_stake( + move_stake.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, origin_netuid=origin_netuid, @@ -3462,7 +3462,7 @@ def stake_transfer( ) return self._run_command( - move.transfer_stake( + move_stake.transfer_stake( wallet=wallet, subtensor=self.initialize_chain(network), origin_netuid=origin_netuid, @@ -3561,7 +3561,7 @@ def stake_swap( amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") return self._run_command( - move.swap_stake( + move_stake.swap_stake( wallet=wallet, subtensor=self.initialize_chain(network), origin_netuid=origin_netuid, diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/list.py similarity index 100% rename from bittensor_cli/src/commands/stake/stake.py rename to bittensor_cli/src/commands/stake/list.py From bad2f5000a678aad5a722ef5c39e5879e5201598 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 18:51:00 -0800 Subject: [PATCH 303/332] Adds safe staking to remove_stake --- bittensor_cli/cli.py | 36 ++- bittensor_cli/src/commands/stake/remove.py | 243 ++++++++++++++++++--- 2 files changed, 241 insertions(+), 38 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c030fb32..3c5bbde9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1371,7 +1371,12 @@ def ask_rate_tolerance( return config_slippage else: console.print( - f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{defaults.rate_tolerance} ({defaults.rate_tolerance*100}%)[/bold cyan] by default. You can set this using `btcli config set`" + "[dim][blue]Rate tolerance[/blue]: " + + f"[bold cyan]{defaults.rate_tolerance} ({defaults.rate_tolerance*100}%)[/bold cyan] " + + "by default. Set this using " + + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + + "or " + + "[dark_sea_green3 italic]`--tolerance`[/dark_sea_green3 italic] flag[/dim]" ) return defaults.rate_tolerance @@ -1402,7 +1407,12 @@ def ask_safe_staking( else: safe_staking = True console.print( - f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] by default. You can set this using `btcli config set`" + "[dim][blue]Safe staking[/blue]: " + + f"[bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] " + + "by default. Set this using " + + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + + "or " + + "[dark_sea_green3 italic]`--safe/--unsafe`[/dark_sea_green3 italic] flag[/dim]" ) return safe_staking @@ -1432,7 +1442,12 @@ def ask_partial_stake( return config_partial else: console.print( - f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] by default. You can set this using `btcli config set`" + "[dim][blue]Partial staking[/blue]: " + + f"[bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] " + + "by default. Set this using " + + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + + "or " + + "[dark_sea_green3 italic]`--partial/--no-partial`[/dark_sea_green3 italic] flag[/dim]" ) return False @@ -3039,6 +3054,9 @@ def stake_remove( help="When set, this command unstakes from all the hotkeys associated with the wallet. Do not use if specifying " "hotkeys in `--include-hotkeys`.", ), + rate_tolerance: Optional[float] = Options.rate_tolerance, + safe_staking: Optional[bool] = Options.safe_staking, + allow_partial_stake: Optional[bool] = Options.allow_partial_stake, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -3061,9 +3079,12 @@ def stake_remove( [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. """ self.verbosity_handler(quiet, verbose) - # # TODO: Coldkey related unstakes need to be updated. Patching for now. - # unstake_all_alpha = False - # unstake_all = False + if not unstake_all and not unstake_all_alpha: + safe_staking = self.ask_safe_staking(safe_staking) + if safe_staking: + rate_tolerance = self.ask_rate_tolerance(rate_tolerance) + allow_partial_stake = self.ask_partial_stake(allow_partial_stake) + console.print("\n") if interactive and any( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] @@ -3202,6 +3223,9 @@ def stake_remove( prompt=prompt, interactive=interactive, netuid=netuid, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, + allow_partial_stake=allow_partial_stake, ) ) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 8dd49ad6..893bab25 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -9,6 +9,7 @@ from rich.prompt import Confirm, Prompt from rich.table import Table +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( @@ -39,8 +40,11 @@ async def unstake( exclude_hotkeys: list[str], amount: float, prompt: bool, - interactive: bool = False, - netuid: Optional[int] = None, + interactive: bool, + netuid: Optional[int], + safe_staking: bool, + rate_tolerance: float, + allow_partial_stake: bool, ): """Unstake from hotkey(s).""" unstake_all_from_hk = False @@ -113,6 +117,7 @@ async def unstake( unstake_operations = [] total_received_amount = Balance.from_tao(0) max_float_slippage = 0 + table_rows = [] for hotkey in hotkeys_to_unstake_from: if skip_remaining_subnets: break @@ -179,8 +184,7 @@ async def unstake( total_received_amount += received_amount max_float_slippage = max(max_float_slippage, slippage_pct_float) - unstake_operations.append( - { + base_unstake_op = { "netuid": netuid, "hotkey_name": staking_address_name if staking_address_name @@ -192,8 +196,42 @@ async def unstake( "slippage_pct": slippage_pct, "slippage_pct_float": slippage_pct_float, "dynamic_info": subnet_info, - } - ) + } + + base_table_row = [ + str(netuid), # Netuid + staking_address_name, # Hotkey Name + str(amount_to_unstake_as_balance), # Amount to Unstake + str(subnet_info.price.tao) + + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate + str(received_amount), # Received Amount + slippage_pct, # Slippage Percent + ] + + # Additional fields for safe unstaking + if safe_staking: + if subnet_info.is_dynamic: + rate = subnet_info.price.tao or 1 + rate_with_tolerance = rate * ( + 1 - rate_tolerance + ) # Rate only for display + price_with_tolerance = subnet_info.price.rao * ( + 1 - rate_tolerance + ) # Actual price to pass to extrinsic + else: + rate_with_tolerance = 1 + price_with_tolerance = 1 + + base_unstake_op["price_with_tolerance"] = price_with_tolerance + base_table_row.extend( + [ + f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", # Rate with tolerance + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # Partial unstake + ] + ) + + unstake_operations.append(base_unstake_op) + table_rows.append(base_table_row) if not unstake_operations: console.print("[red]No unstake operations to perform.[/red]") @@ -204,25 +242,17 @@ async def unstake( wallet_coldkey_ss58=wallet.coldkeypub.ss58_address, network=subtensor.network, total_received_amount=total_received_amount, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, ) + for row in table_rows: + table.add_row(*row) - for op in unstake_operations: - subnet_info = op["dynamic_info"] - table.add_row( - str(op["netuid"]), # Netuid - op["hotkey_name"], # Hotkey Name - str(op["amount_to_unstake"]), # Amount to Unstake - str(float(subnet_info.price)) - + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})", # Rate - str(op["received_amount"]), # Received Amount - op["slippage_pct"], # Slippage Percent - ) - - _print_table_and_slippage(table, max_float_slippage) + _print_table_and_slippage(table, max_float_slippage, safe_staking) if prompt: if not Confirm.ask("Would you like to continue?"): raise typer.Exit() - + # Execute extrinsics try: wallet.unlock_coldkey() @@ -233,20 +263,33 @@ async def unstake( with console.status("\n:satellite: Performing unstaking operations...") as status: if unstake_all_from_hk: await _unstake_all_extrinsic( - hotkey_ss58=unstake_all_hk_ss58, wallet=wallet, subtensor=subtensor, + hotkey_ss58=unstake_all_hk_ss58, status=status, ) - else: + elif safe_staking: for op in unstake_operations: - await _unstake_extrinsic( + await _safe_unstake_extrinsic( + wallet=wallet, + subtensor=subtensor, netuid=op["netuid"], amount=op["amount_to_unstake"], current_stake=op["current_stake_balance"], hotkey_ss58=op["hotkey_ss58"], + price_limit=op["price_with_tolerance"], + allow_partial_stake=allow_partial_stake, + status=status, + ) + else: + for op in unstake_operations: + await _unstake_extrinsic( wallet=wallet, subtensor=subtensor, + netuid=op["netuid"], + amount=op["amount_to_unstake"], + current_stake=op["current_stake_balance"], + hotkey_ss58=op["hotkey_ss58"], status=status, ) console.print( @@ -444,12 +487,12 @@ async def unstake_all( # Extrinsics async def _unstake_extrinsic( + wallet: Wallet, + subtensor: "SubtensorInterface", netuid: int, amount: Balance, current_stake: Balance, hotkey_ss58: str, - wallet: Wallet, - subtensor: "SubtensorInterface", status=None, ) -> None: """Execute a standard unstake extrinsic. @@ -526,9 +569,9 @@ async def _unstake_extrinsic( async def _unstake_all_extrinsic( - hotkey_ss58: str, wallet: Wallet, subtensor: "SubtensorInterface", + hotkey_ss58: str, status=None, ) -> None: """Execute an unstake_all extrinsic. @@ -574,6 +617,125 @@ async def _unstake_all_extrinsic( print_error(f":cross_mark: [red]Failed[/red] with error: {str(e)}", status) +async def _safe_unstake_extrinsic( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + amount: Balance, + current_stake: Balance, + hotkey_ss58: str, + price_limit: Balance, + allow_partial_stake: bool, + status=None, +) -> None: + """Execute a safe unstake extrinsic with price limit. + + Args: + netuid: The subnet ID + amount: Amount to unstake + current_stake: Current stake balance + hotkey_ss58: Hotkey SS58 address + price_limit: Maximum acceptable price + wallet: Wallet instance + subtensor: Subtensor interface + allow_partial_stake: Whether to allow partial unstaking + status: Optional status for console updates + """ + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" + ) + + if status: + status.update( + f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." + ) + + block_hash = await subtensor.substrate.get_chain_head() + + current_balance, next_nonce, current_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + ), + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + ) + + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce + ) + + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + except SubstrateRequestException as e: + if "Custom error: 8" in str(e): + print_error( + f"\n{failure_prelude}: Price exceeded tolerance limit. " + f"Transaction rejected because partial unstaking is disabled. " + f"Either increase price tolerance or enable partial unstaking.", + status=status, + ) + return + else: + err_out( + f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + ) + return + + await response.process_events() + if not await response.is_success: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + ) + return + + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block_hash=block_hash, + ), + ) + + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + + amount_unstaked = current_stake - new_stake + if allow_partial_stake and (amount_unstaked != amount): + console.print( + "Partial unstake transaction. Unstaked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount}[/blue]" + ) + + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + + # Helpers def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: """Calculate slippage and received amount for unstaking operation. @@ -902,6 +1064,8 @@ def _create_unstake_table( wallet_coldkey_ss58: str, network: str, total_received_amount: Balance, + safe_staking: bool, + rate_tolerance: float, ) -> Table: """Create a table summarizing unstake operations. @@ -955,6 +1119,17 @@ def _create_unstake_table( table.add_column( "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] ) + if safe_staking: + table.add_column( + f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Partial unstake enabled", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) return table @@ -962,6 +1137,7 @@ def _create_unstake_table( def _print_table_and_slippage( table: Table, max_float_slippage: float, + safe_staking: bool, ) -> None: """Print the unstake summary table and additional information. @@ -979,16 +1155,19 @@ def _print_table_and_slippage( " this may result in a loss of funds.\n" f"-------------------------------------------------------------------------------------------------------------------\n" ) - console.print( - """ + base_description = """ [bold white]Description[/bold white]: The table displays information about the stake remove operation you are about to perform. The columns are as follows: - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. - - [bold white]Amount[/bold white]: The stake amount you are removing from this key. + - [bold white]Amount to Unstake[/bold white]: The stake amount you are removing from this key. - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). -""" - ) + - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root).""" + + safe_staking_description = """ + - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate reduces below this tolerance, the transaction will be limited or rejected. + - [bold white]Partial unstaking[/bold white]: If True, allows unstaking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" + + console.print(base_description + (safe_staking_description if safe_staking else "")) From 69a6ca90c52d9fb213499a5f99b6416fb1468653 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 21:09:53 -0800 Subject: [PATCH 304/332] Fixes unstaking with safe staking --- bittensor_cli/cli.py | 88 ++-- bittensor_cli/src/commands/stake/add.py | 454 ++++++++++----------- bittensor_cli/src/commands/stake/remove.py | 119 ++---- 3 files changed, 331 insertions(+), 330 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3c5bbde9..85779249 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3160,6 +3160,52 @@ def stake_remove( validate=WV.WALLET_AND_HOTKEY, ) + elif unstake_all or unstake_all_alpha: + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if include_hotkeys: + if len(include_hotkeys) > 1: + print_error("Cannot unstake_all from multiple hotkeys at once.") + raise typer.Exit() + elif is_valid_ss58_address(include_hotkeys[0]): + hotkey_ss58_address = include_hotkeys[0] + else: + print_error("Invalid hotkey ss58 address.") + raise typer.Exit() + else: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + ) + if is_valid_ss58_address(hotkey_or_ss58): + hotkey_ss58_address = hotkey_or_ss58 + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + else: + wallet_hotkey = hotkey_or_ss58 + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + return self._run_command( + remove_stake.unstake_all( + wallet=wallet, + subtensor=self.initialize_chain(network), + hotkey_ss58_address=hotkey_ss58_address, + unstake_all_alpha=unstake_all_alpha, + prompt=prompt, + ) + ) elif ( all_hotkeys or include_hotkeys @@ -3201,33 +3247,23 @@ def stake_remove( else: excluded_hotkeys = [] - if unstake_all or unstake_all_alpha: - return self._run_command( - remove_stake.unstake_all( - wallet=wallet, - subtensor=self.initialize_chain(network), - unstake_all_alpha=unstake_all_alpha, - prompt=prompt, - ) - ) - else: - return self._run_command( - remove_stake.unstake( - wallet=wallet, - subtensor=self.initialize_chain(network), - hotkey_ss58_address=hotkey_ss58_address, - all_hotkeys=all_hotkeys, - include_hotkeys=included_hotkeys, - exclude_hotkeys=excluded_hotkeys, - amount=amount, - prompt=prompt, - interactive=interactive, - netuid=netuid, - safe_staking=safe_staking, - rate_tolerance=rate_tolerance, - allow_partial_stake=allow_partial_stake, - ) + return self._run_command( + remove_stake.unstake( + wallet=wallet, + subtensor=self.initialize_chain(network), + hotkey_ss58_address=hotkey_ss58_address, + all_hotkeys=all_hotkeys, + include_hotkeys=included_hotkeys, + exclude_hotkeys=excluded_hotkeys, + amount=amount, + prompt=prompt, + interactive=interactive, + netuid=netuid, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, + allow_partial_stake=allow_partial_stake, ) + ) def stake_move( self, diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index f2067cbb..f6f161f0 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -25,233 +25,6 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -# Helper functions -def _prompt_stake_amount( - current_balance: Balance, netuid: int, action_name: str -) -> tuple[Balance, bool]: - """Prompts user to input a stake amount with validation. - - Args: - current_balance (Balance): The maximum available balance - netuid (int): The subnet id to get the correct unit - action_name (str): The name of the action (e.g. "transfer", "move", "unstake") - - Returns: - tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected) - """ - while True: - amount_input = Prompt.ask( - f"\nEnter the amount to {action_name}" - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"or " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"for entire balance" - ) - - if amount_input.lower() == "all": - return current_balance, True - - try: - amount = float(amount_input) - if amount <= 0: - console.print("[red]Amount must be greater than 0[/red]") - continue - if amount > current_balance.tao: - console.print( - f"[red]Amount exceeds available balance of " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f"[/red]" - ) - continue - return Balance.from_tao(amount), False - except ValueError: - console.print("[red]Please enter a valid number or 'all'[/red]") - - -def _get_hotkeys_to_stake_to( - wallet: Wallet, - all_hotkeys: bool = False, - include_hotkeys: list[str] = None, - exclude_hotkeys: list[str] = None, -) -> list[tuple[Optional[str], str]]: - """Get list of hotkeys to stake to based on input parameters. - - Args: - wallet: The wallet containing hotkeys - all_hotkeys: If True, get all hotkeys from wallet except excluded ones - include_hotkeys: List of specific hotkeys to include (by name or ss58 address) - exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True - - Returns: - List of tuples containing (hotkey_name, hotkey_ss58_address) - hotkey_name may be None if ss58 address was provided directly - """ - if all_hotkeys: - # Stake to all hotkeys except excluded ones - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - return [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in (exclude_hotkeys or []) - ] - - if include_hotkeys: - print_verbose("Staking to only included hotkeys") - # Stake to specific hotkeys - hotkeys = [] - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If valid ss58 address, add directly - hotkeys.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If hotkey name, get ss58 from wallet - wallet_ = Wallet( - path=wallet.path, - name=wallet.name, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) - - return hotkeys - - # Default: stake to single hotkey from wallet - print_verbose( - f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" - ) - assert wallet.hotkey is not None - return [(None, wallet.hotkey.ss58_address)] - - -def _define_stake_table( - wallet: Wallet, - subtensor: "SubtensorInterface", - safe_staking: bool, - rate_tolerance: float, -) -> Table: - """Creates and initializes a table for displaying stake information. - - Args: - wallet: The wallet being used for staking - subtensor: The subtensor interface - - Returns: - Table: An initialized rich Table object with appropriate columns - """ - table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Amount ({Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO"], - ) - table.add_column( - f"Rate (per {Balance.get_unit(0)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - "Received", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], - ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) - - if safe_staking: - table.add_column( - f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - "Partial stake enabled", - justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - ) - return table - - -def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool): - """Prints the stake table, slippage warning, and table description. - - Args: - table: The rich Table object to print - max_slippage: The maximum slippage percentage across all operations - """ - console.print(table) - - # Greater than 5% - if max_slippage > 5: - message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - - # Table description - base_description = """ -[bold white]Description[/bold white]: -The table displays information about the stake operation you are about to perform. -The columns are as follows: - - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. - - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. - - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" - - safe_staking_description = """ - - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. - - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" - - console.print(base_description + (safe_staking_description if safe_staking else "")) - - -def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: - """Calculate slippage when adding stake. - - Args: - subnet_info: Subnet dynamic info - amount: Amount being staked - - Returns: - tuple containing: - - received_amount: Amount received after slippage - - slippage_str: Formatted slippage percentage string - - slippage_float: Raw slippage percentage value - """ - received_amount, _, slippage_pct_float = subnet_info.tao_to_alpha_with_slippage( - amount - ) - if subnet_info.is_dynamic: - slippage_str = f"{slippage_pct_float:.4f} %" - rate = f"{(1 / subnet_info.price.tao or 1):.4f}" - else: - slippage_pct_float = 0 - slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" - rate = "1" - - return received_amount, slippage_str, slippage_pct_float, rate - - # Command async def stake_add( wallet: Wallet, @@ -623,3 +396,230 @@ async def stake_extrinsic( # We can gather them all at once but balance reporting will be in race-condition. for coroutine in stake_coroutines: await coroutine + + +# Helper functions +def _prompt_stake_amount( + current_balance: Balance, netuid: int, action_name: str +) -> tuple[Balance, bool]: + """Prompts user to input a stake amount with validation. + + Args: + current_balance (Balance): The maximum available balance + netuid (int): The subnet id to get the correct unit + action_name (str): The name of the action (e.g. "transfer", "move", "unstake") + + Returns: + tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected) + """ + while True: + amount_input = Prompt.ask( + f"\nEnter the amount to {action_name}" + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"or " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"for entire balance" + ) + + if amount_input.lower() == "all": + return current_balance, True + + try: + amount = float(amount_input) + if amount <= 0: + console.print("[red]Amount must be greater than 0[/red]") + continue + if amount > current_balance.tao: + console.print( + f"[red]Amount exceeds available balance of " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"[/red]" + ) + continue + return Balance.from_tao(amount), False + except ValueError: + console.print("[red]Please enter a valid number or 'all'[/red]") + + +def _get_hotkeys_to_stake_to( + wallet: Wallet, + all_hotkeys: bool = False, + include_hotkeys: list[str] = None, + exclude_hotkeys: list[str] = None, +) -> list[tuple[Optional[str], str]]: + """Get list of hotkeys to stake to based on input parameters. + + Args: + wallet: The wallet containing hotkeys + all_hotkeys: If True, get all hotkeys from wallet except excluded ones + include_hotkeys: List of specific hotkeys to include (by name or ss58 address) + exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True + + Returns: + List of tuples containing (hotkey_name, hotkey_ss58_address) + hotkey_name may be None if ss58 address was provided directly + """ + if all_hotkeys: + # Stake to all hotkeys except excluded ones + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + return [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in (exclude_hotkeys or []) + ] + + if include_hotkeys: + print_verbose("Staking to only included hotkeys") + # Stake to specific hotkeys + hotkeys = [] + for hotkey_ss58_or_hotkey_name in include_hotkeys: + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + # If valid ss58 address, add directly + hotkeys.append((None, hotkey_ss58_or_hotkey_name)) + else: + # If hotkey name, get ss58 from wallet + wallet_ = Wallet( + path=wallet.path, + name=wallet.name, + hotkey=hotkey_ss58_or_hotkey_name, + ) + hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) + + return hotkeys + + # Default: stake to single hotkey from wallet + print_verbose( + f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" + ) + assert wallet.hotkey is not None + return [(None, wallet.hotkey.ss58_address)] + + +def _define_stake_table( + wallet: Wallet, + subtensor: "SubtensorInterface", + safe_staking: bool, + rate_tolerance: float, +) -> Table: + """Creates and initializes a table for displaying stake information. + + Args: + wallet: The wallet being used for staking + subtensor: The subtensor interface + + Returns: + Table: An initialized rich Table object with appropriate columns + """ + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n" + f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " + f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate (per {Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Received", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + + if safe_staking: + table.add_column( + f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Partial stake enabled", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) + return table + + +def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool): + """Prints the stake table, slippage warning, and table description. + + Args: + table: The rich Table object to print + max_slippage: The maximum slippage percentage across all operations + """ + console.print(table) + + # Greater than 5% + if max_slippage > 5: + message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + # Table description + base_description = """ +[bold white]Description[/bold white]: +The table displays information about the stake operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. + - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. + - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" + + safe_staking_description = """ + - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. + - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" + + console.print(base_description + (safe_staking_description if safe_staking else "")) + + +def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: + """Calculate slippage when adding stake. + + Args: + subnet_info: Subnet dynamic info + amount: Amount being staked + + Returns: + tuple containing: + - received_amount: Amount received after slippage + - slippage_str: Formatted slippage percentage string + - slippage_float: Raw slippage percentage value + """ + received_amount, _, slippage_pct_float = subnet_info.tao_to_alpha_with_slippage( + amount + ) + if subnet_info.is_dynamic: + slippage_str = f"{slippage_pct_float:.4f} %" + rate = f"{(1 / subnet_info.price.tao or 1):.4f}" + else: + slippage_pct_float = 0 + slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" + rate = "1" + + return received_amount, slippage_str, slippage_pct_float, rate diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 893bab25..dcce01bc 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -68,6 +68,20 @@ async def unstake( old_identities, netuid=netuid, ) + if unstake_all_from_hk: + hotkey_to_unstake_all = hotkeys_to_unstake_from[0] + unstake_all_alpha = Confirm.ask( + "\nUnstake [blue]all alpha stakes[/blue] and stake back to [blue]root[/blue]? (No will unstake everything)", + default=True, + ) + return await unstake_all( + wallet=wallet, + subtensor=subtensor, + hotkey_ss58_address=hotkey_to_unstake_all[1], + unstake_all_alpha=unstake_all_alpha, + prompt=prompt, + ) + if not hotkeys_to_unstake_from: console.print("[red]No unstake operations to perform.[/red]") return False @@ -113,7 +127,6 @@ async def unstake( ) # Iterate over hotkeys and netuids to collect unstake operations - unstake_all_hk_ss58 = None unstake_operations = [] total_received_amount = Balance.from_tao(0) max_float_slippage = 0 @@ -150,10 +163,7 @@ async def unstake( continue # No stake to unstake # Determine the amount we are unstaking. - if unstake_all_from_hk: - amount_to_unstake_as_balance = current_stake_balance - unstake_all_hk_ss58 = staking_address_ss58 - elif initial_amount: + if initial_amount: amount_to_unstake_as_balance = Balance.from_tao(initial_amount) else: amount_to_unstake_as_balance = _ask_unstake_amount( @@ -185,17 +195,17 @@ async def unstake( max_float_slippage = max(max_float_slippage, slippage_pct_float) base_unstake_op = { - "netuid": netuid, - "hotkey_name": staking_address_name - if staking_address_name - else staking_address_ss58, - "hotkey_ss58": staking_address_ss58, - "amount_to_unstake": amount_to_unstake_as_balance, - "current_stake_balance": current_stake_balance, - "received_amount": received_amount, - "slippage_pct": slippage_pct, - "slippage_pct_float": slippage_pct_float, - "dynamic_info": subnet_info, + "netuid": netuid, + "hotkey_name": staking_address_name + if staking_address_name + else staking_address_ss58, + "hotkey_ss58": staking_address_ss58, + "amount_to_unstake": amount_to_unstake_as_balance, + "current_stake_balance": current_stake_balance, + "received_amount": received_amount, + "slippage_pct": slippage_pct, + "slippage_pct_float": slippage_pct_float, + "dynamic_info": subnet_info, } base_table_row = [ @@ -221,7 +231,7 @@ async def unstake( else: rate_with_tolerance = 1 price_with_tolerance = 1 - + base_unstake_op["price_with_tolerance"] = price_with_tolerance base_table_row.extend( [ @@ -261,14 +271,7 @@ async def unstake( return False with console.status("\n:satellite: Performing unstaking operations...") as status: - if unstake_all_from_hk: - await _unstake_all_extrinsic( - wallet=wallet, - subtensor=subtensor, - hotkey_ss58=unstake_all_hk_ss58, - status=status, - ) - elif safe_staking: + if safe_staking: for op in unstake_operations: await _safe_unstake_extrinsic( wallet=wallet, @@ -300,6 +303,7 @@ async def unstake( async def unstake_all( wallet: Wallet, subtensor: "SubtensorInterface", + hotkey_ss58_address: str, unstake_all_alpha: bool = False, prompt: bool = True, ) -> bool: @@ -322,6 +326,11 @@ async def unstake_all( subtensor.all_subnets(), subtensor.get_balance(wallet.coldkeypub.ss58_address), ) + if not hotkey_ss58_address: + hotkey_ss58_address = wallet.hotkey.ss58_address + stake_info = [ + stake for stake in stake_info if stake.hotkey_ss58 == hotkey_ss58_address + ] if unstake_all_alpha: stake_info = [stake for stake in stake_info if stake.netuid != 0] @@ -443,12 +452,17 @@ async def unstake_all( if unstake_all_alpha else ":satellite: Unstaking all stakes..." ) + previous_root_stake = await subtensor.get_stake( + hotkey_ss58=hotkey_ss58_address, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=0, + ) with console.status(console_status): call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function=call_function, - call_params={"hotkey": wallet.hotkey.ss58_address}, + call_params={"hotkey": hotkey_ss58_address}, ) success, error_message = await subtensor.sign_and_send_extrinsic( call=call, @@ -470,12 +484,12 @@ async def unstake_all( ) if unstake_all_alpha: root_stake = await subtensor.get_stake( - hotkey_ss58=wallet.hotkey.ss58_address, + hotkey_ss58=hotkey_ss58_address, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=0, ) console.print( - f"Root Stake:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{root_stake}" + f"Root Stake:\n [blue]{previous_root_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{root_stake}" ) return True else: @@ -568,55 +582,6 @@ async def _unstake_extrinsic( err_out(f"{failure_prelude} with error: {str(e)}") -async def _unstake_all_extrinsic( - wallet: Wallet, - subtensor: "SubtensorInterface", - hotkey_ss58: str, - status=None, -) -> None: - """Execute an unstake_all extrinsic. - - Args: - hotkey_ss58: Hotkey SS58 address - wallet: Wallet instance - subtensor: Subtensor interface - status: Optional status for console updates - """ - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="unstake_all", - call_params={"hotkey": hotkey_ss58}, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - await response.process_events() - - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - return - - new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - - except Exception as e: - print_error(f":cross_mark: [red]Failed[/red] with error: {str(e)}", status) - - async def _safe_unstake_extrinsic( wallet: Wallet, subtensor: "SubtensorInterface", From d61df2a243dd44781a523264a741fe71a0c35947 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 10 Feb 2025 21:26:33 -0800 Subject: [PATCH 305/332] Updates root unstake --- bittensor_cli/src/commands/stake/remove.py | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index dcce01bc..cbede0c3 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -273,17 +273,28 @@ async def unstake( with console.status("\n:satellite: Performing unstaking operations...") as status: if safe_staking: for op in unstake_operations: - await _safe_unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - price_limit=op["price_with_tolerance"], - allow_partial_stake=allow_partial_stake, - status=status, - ) + if op["netuid"] == 0: + await _unstake_extrinsic( + wallet=wallet, + subtensor=subtensor, + netuid=op["netuid"], + amount=op["amount_to_unstake"], + current_stake=op["current_stake_balance"], + hotkey_ss58=op["hotkey_ss58"], + status=status, + ) + else: + await _safe_unstake_extrinsic( + wallet=wallet, + subtensor=subtensor, + netuid=op["netuid"], + amount=op["amount_to_unstake"], + current_stake=op["current_stake_balance"], + hotkey_ss58=op["hotkey_ss58"], + price_limit=op["price_with_tolerance"], + allow_partial_stake=allow_partial_stake, + status=status, + ) else: for op in unstake_operations: await _unstake_extrinsic( From 2137dc3878913c7fc951a13e321fcaa672a22e7f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 11 Feb 2025 13:08:56 +0200 Subject: [PATCH 306/332] coldkey ss58 list needs to be in a list of its own --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 8c464062..d34d5fb6 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1330,7 +1330,7 @@ async def get_stake_for_coldkeys( result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkeys", - params=coldkey_ss58_list, + params=[coldkey_ss58_list], block_hash=block_hash, ) if result is None: From fa8dc509ca352d26d85eb630942328b0d449e56d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 11 Feb 2025 17:53:33 +0200 Subject: [PATCH 307/332] Fixed key unlock during signing + added unlocking for hotkey in the case of an encrypted hotkey. --- bittensor_cli/src/commands/wallets.py | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4d0b339b..7cdf7582 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -6,7 +6,7 @@ import aiohttp from bittensor_wallet import Wallet, Keypair -from bittensor_wallet.errors import KeyFileError +from bittensor_wallet.errors import KeyFileError, PasswordError from bittensor_wallet.keyfile import Keyfile from fuzzywuzzy import fuzz from rich import box @@ -1418,19 +1418,31 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): async def sign(wallet: Wallet, message: str, use_hotkey: str): """Sign a message using the provided wallet or hotkey.""" - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is " - "invalid[/red]:[bold white]\n [/bold white]" - ) + def _unlock(key: str): + try: + getattr(wallet, f"unlock_{key}")() + return True + except PasswordError: + err_console.print( + ":cross_mark: [red]The password used to decrypt your keyfile is invalid[/red]" + ) + return False + except KeyFileError: + err_console.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, or non-readable[/red]:" + ) + return False + if not use_hotkey: + if not _unlock("coldkey"): + return False keypair = wallet.coldkey print_verbose( f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}" ) else: + if not _unlock("hotkey"): + return False keypair = wallet.hotkey print_verbose( f"Signing using [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey: {wallet.hotkey_str}" From ba37e0ec9d9aabe63ea8461936bd1d1e0d5555e2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 11 Feb 2025 09:16:48 -0800 Subject: [PATCH 308/332] cleanup --- bittensor_cli/src/commands/stake/list.py | 1 - bittensor_cli/src/commands/stake/remove.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index fb8dfed1..9f2ba776 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -15,7 +15,6 @@ from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( - # TODO add back in caching console, print_error, millify_tao, diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index cbede0c3..80d77a6a 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -13,7 +13,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( - # TODO add back in caching console, err_console, print_verbose, @@ -22,8 +21,6 @@ is_valid_ss58_address, format_error_message, group_subnets, - millify_tao, - get_subnet_name, ) if TYPE_CHECKING: From 288a5a5b15b45983156a3ddf16446831e7496ad4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 11 Feb 2025 18:06:57 -0800 Subject: [PATCH 309/332] Removes stake from w balances --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/wallets.py | 28 ++++----------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 85779249..99583699 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2963,7 +2963,7 @@ def stake_add( # TODO: Ask amount for each subnet explicitly if more than one if not stake_all and not amount: - free_balance, staked_balance = self._run_command( + free_balance = self._run_command( wallets.wallet_balance( wallet, self.initialize_chain(network), False, None ), diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 7cdf7582..f1d648ed 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -286,16 +286,12 @@ async def wallet_balance( wallet_names = [wallet.name] block_hash = await subtensor.substrate.get_chain_head() - free_balances, staked_balances = await asyncio.gather( - subtensor.get_balances(*coldkeys, block_hash=block_hash), - subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), - ) + free_balances = await subtensor.get_balances(*coldkeys, block_hash=block_hash) total_free_balance = sum(free_balances.values()) - total_staked_balance = sum(staked_balances.values()) balances = { - name: (coldkey, free_balances[coldkey], staked_balances[coldkey]) + name: (coldkey, free_balances[coldkey]) for (name, coldkey) in zip(wallet_names, coldkeys) } @@ -316,18 +312,6 @@ async def wallet_balance( style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), - Column( - "[white]Staked Balance", - justify="right", - style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], - no_wrap=True, - ), - Column( - "[white]Total Balance", - justify="right", - style=COLOR_PALETTE["GENERAL"]["BALANCE"], - no_wrap=True, - ), title=f"\n [{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance\nNetwork: {subtensor.network}", show_footer=True, show_edge=False, @@ -338,25 +322,21 @@ async def wallet_balance( leading=True, ) - for name, (coldkey, free, staked) in balances.items(): + for name, (coldkey, free) in balances.items(): table.add_row( name, coldkey, str(free), - str(staked), - str(free + staked), ) table.add_row() table.add_row( "Total Balance", "", str(total_free_balance), - str(total_staked_balance), - str(total_free_balance + total_staked_balance), ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() - return total_free_balance, total_staked_balance + return total_free_balance async def get_wallet_transfers(wallet_address: str) -> list[dict]: From 3fc32df8405a2cb1afa5c16304be5f8afc39d335 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 11 Feb 2025 18:52:00 -0800 Subject: [PATCH 310/332] Updates display when prompting stake for movement --- bittensor_cli/src/commands/stake/move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index e83706ab..5ce500f1 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -178,7 +178,7 @@ def prompt_stake_amount( """ while True: amount_input = Prompt.ask( - f"\nEnter the amount to {action_name}" + f"\nEnter the amount to {action_name} " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " f"or " From 7f13dc39a21f4ad17d22adca8b44c3bf64f8ccba Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 11 Feb 2025 20:13:57 -0800 Subject: [PATCH 311/332] Updates docstrings for commands --- bittensor_cli/cli.py | 204 ++++++++++++--------- bittensor_cli/src/commands/stake/add.py | 2 +- bittensor_cli/src/commands/stake/remove.py | 2 +- 3 files changed, 123 insertions(+), 85 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 99583699..bd364c85 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -176,13 +176,13 @@ class Options: ) netuid = typer.Option( None, - help="The netuid of the subnet in the root network, (e.g. 1).", + help="The netuid of the subnet in the network, (e.g. 1).", prompt=True, callback=validate_netuid, ) netuid_not_req = typer.Option( None, - help="The netuid of the subnet in the root network, (e.g. 1).", + help="The netuid of the subnet in the network, (e.g. 1).", prompt=False, ) all_netuids = typer.Option( @@ -957,9 +957,12 @@ async def _run(): ConnectionClosed, SubstrateRequestException, KeyboardInterrupt, + RuntimeError, ) as e: if isinstance(e, SubstrateRequestException): err_console.print(str(e)) + elif isinstance(e, RuntimeError): + pass # Temporarily to handle loop bound issues verbose_console.print(traceback.format_exc()) except Exception as e: err_console.print(f"An unknown error has occurred: {e}") @@ -1118,7 +1121,23 @@ def set_config( ), ): """ - Sets the values in the config file. To set the metagraph configuration, use the command `btcli config metagraph` + Sets or updates configuration values in the BTCLI config file. + + This command allows you to set default values that will be used across all BTCLI commands. + + USAGE + Interactive mode: + [green]$[/green] btcli config set + + Set specific values: + [green]$[/green] btcli config set --wallet-name default --network finney + [green]$[/green] btcli config set --safe-staking --rate-tolerance 0.1 + + [bold]NOTE[/bold]: + - Network values can be network names (e.g., 'finney', 'test') or websocket URLs + - Rate tolerance is specified as a decimal (e.g., 0.05 for 0.05%) + - Changes are saved to ~/.bittensor/btcli.yaml + - Use '[green]$[/green] btcli config get' to view current settings """ args = { "wallet_name": wallet_name, @@ -1607,50 +1626,9 @@ def wallet_overview( USAGE - The command offers various options to customize the output. Users can filter the displayed data by specific - netuid, sort by different criteria, and choose to include all the wallets in the user's wallet path location. - The output is presented in a tabular format with the following columns: - - - COLDKEY: The SS58 address of the coldkey. - - - HOTKEY: The SS58 address of the hotkey. - - - UID: Unique identifier of the neuron. - - - ACTIVE: Indicates if the neuron is active. - - - STAKE(τ): Amount of stake in the neuron, in TAO. - - - RANK: The rank of the neuron within the network. - - - TRUST: Trust score of the neuron. - - - CONSENSUS: Consensus score of the neuron. - - - INCENTIVE: Incentive score of the neuron. - - - DIVIDENDS: Dividends earned by the neuron. - - - EMISSION(p): Emission received by the neuron, expressed in rho. - - - VTRUST: Validator trust score of the neuron. - - - VPERMIT: Indicates if the neuron has a validator permit. - - - UPDATED: Time since last update. - - - AXON: IP address and port of the neuron. - - - HOTKEY_SS58: Human-readable representation of the hotkey. - - - # EXAMPLE: - [green]$[/green] btcli wallet overview - [green]$[/green] btcli wallet overview --all --sort-by stake --sort-order descending - - [green]$[/green] btcli wallet overview -in hk1,hk2 --sort-by stake + [green]$[/green] btcli wallet overview --all [bold]NOTE[/bold]: This command is read-only and does not modify the blockchain state or account configuration. It provides a quick and comprehensive view of the user's network presence, making it useful for monitoring account status, @@ -2758,7 +2736,25 @@ def stake_list( no_prompt: bool = Options.prompt, # TODO add: all-wallets, reuse_last, html_output ): - """List all stake accounts for wallet.""" + """ + Display detailed stake information for a wallet across all subnets. + + Shows stake allocations, exchange rates, and emissions for each hotkey. + + [bold]Common Examples:[/bold] + + 1. Basic stake overview: + [green]$[/green] btcli stake list --wallet.name my_wallet + + 2. Live updating view with refresh: + [green]$[/green] btcli stake list --wallet.name my_wallet --live + + 3. View specific coldkey by address: + [green]$[/green] btcli stake list --ss58 5Dk...X3q + + 4. Verbose output with full values: + [green]$[/green] btcli stake list --wallet.name my_wallet --verbose + """ self.verbosity_handler(quiet, verbose) wallet = None @@ -2838,21 +2834,42 @@ def stake_add( verbose: bool = Options.verbose, ): """ - Stake TAO to one or more hotkeys associated with the user's coldkey. + Stake TAO to one or more hotkeys on specific netuids with your coldkey. - This command is used by a subnet validator to stake to their own hotkey. Compare this command with "btcli root delegate" that is typically run by a TAO holder to delegate their TAO to a delegate's hotkey. + Stake is always added through your coldkey's free balance. For stake movement, please see `[green]$[/green] btcli stake move` command. - This command is used by a subnet validator to allocate stake TAO to their different hotkeys, securing their position and influence on the network. + [bold]Common Examples:[/bold] - EXAMPLE + 1. Interactive staking (guided prompts): + [green]$[/green] btcli stake add + + 2. Safe staking with rate tolerance of 10% with partial transaction disabled: + [green]$[/green] btcli stake add --amount 100 --netuid 1 --safe --tolerance 0.1 --no-partial + + 3. Allow partial stake if rates change with tolerance of 10%: + [green]$[/green] btcli stake add --amount 300 --safe --partial --tolerance 0.1 + + 4. Unsafe staking with no rate protection: + [green]$[/green] btcli stake add --amount 300 --netuid 1 --unsafe + + 5. Stake to multiple hotkeys: + [green]$[/green] btcli stake add --amount 200 --include-hotkeys hk_ss58_1,hk_ss58_2,hk_ss58_3 + + 6. Stake all balance to a subnet: + [green]$[/green] btcli stake add --all --netuid 3 + + [bold]Safe Staking Parameters:[/bold] + • [blue]--safe[/blue]: Enables rate tolerance checks + • [blue]--tolerance[/blue]: Maximum % rate change allowed (0.05 = 5%) + • [blue]--partial[/blue]: Complete partial stake if rates exceed tolerance - [green]$[/green] btcli stake add --amount 100 --wallet-name --wallet-hotkey """ self.verbosity_handler(quiet, verbose) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) allow_partial_stake = self.ask_partial_stake(allow_partial_stake) + console.print("\n") netuid = get_optional_netuid(netuid, all_netuids) if stake_all and amount: @@ -3068,15 +3085,34 @@ def stake_remove( verbose: bool = Options.verbose, ): """ - Unstake TAO from one or more hotkeys and transfer them back to the user's coldkey. + Unstake TAO from one or more hotkeys and transfer them back to the user's coldkey wallet. - This command is used to withdraw TAO previously staked to different hotkeys. + This command is used to withdraw TAO or Alpha stake from different hotkeys. - EXAMPLE + [bold]Common Examples:[/bold] + + 1. Interactive unstaking (guided prompts): + [green]$[/green] btcli stake remove - [green]$[/green] btcli stake remove --amount 100 -in hk1,hk2 + 2. Safe unstaking with 10% rate tolerance: + [green]$[/green] btcli stake remove --amount 100 --netuid 1 --safe --tolerance 0.1 - [blue bold]Note[/blue bold]: This command is for users who wish to reallocate their stake or withdraw them from the network. It allows for flexible management of TAO stake across different neurons (hotkeys) on the network. + 3. Allow partial unstake if rates change: + [green]$[/green] btcli stake remove --amount 300 --safe --partial + + 4. Unstake from multiple hotkeys: + [green]$[/green] btcli stake remove --amount 200 --include-hotkeys hk1,hk2,hk3 + + 5. Unstake all from a hotkey: + [green]$[/green] btcli stake remove --all + + 6. Unstake all Alpha from a hotkey and stake to Root: + [green]$[/green] btcli stake remove --all-alpha + + [bold]Safe Staking Parameters:[/bold] + • [blue]--safe[/blue]: Enables rate tolerance checks during unstaking + • [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) + • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ self.verbosity_handler(quiet, verbose) if not unstake_all and not unstake_all_alpha: @@ -3982,8 +4018,6 @@ def sudo_get( """ Shows a list of the hyperparameters for the specified subnet. - The output of this command is the same as that of `btcli subnets hyperparameters`. - EXAMPLE [green]$[/green] btcli sudo get --netuid 1 @@ -4159,40 +4193,37 @@ def sudo_get_take( def subnets_list( self, network: Optional[list[str]] = Options.network, - # reuse_last: bool = Options.reuse_last, - # html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, live_mode: bool = Options.live, ): """ - List all subnets and their detailed information. + List all subnets and their detailed information. - This command displays a table with the below columns: + [bold]Common Examples:[/bold] - - NETUID: The subnet's netuid. - - N: The number of neurons (subnet validators and subnet miners) in the subnet. - - MAX_N: The maximum allowed number of neurons in the subnet. - - EMISSION: The percentage of emissions to the subnet as of the last tempo. - - TEMPO: The subnet's tempo, expressed in number of blocks. - - RECYCLE: The recycle register cost for this subnet. - - POW: The proof of work (PoW) difficulty. - - SUDO: The subnet owner's name or the owner's ss58 address. + 1. List all subnets: + [green]$[/green] btcli subnets list - EXAMPLE + 2. List all subnets in live mode: + [green]$[/green] btcli subnets list --live - [green]$[/green] btcli subnets list + [bold]Output Columns:[/bold] + • [white]Netuid[/white] - Subnet identifier number + • [white]Name[/white] - Subnet name with currency symbol (τ/α/β etc) + • [white]Price (τ_in/α_in)[/white] - Exchange rate (TAO per alpha token) + • [white]Market Cap (α * Price)[/white] - Total value in TAO (alpha tokens × price) + • [white]Emission (τ)[/white] - TAO rewards emitted per block to subnet + • [white]P (τ_in, α_in)[/white] - Pool reserves (Tao reserves, alpha reserves) in liquidity pool + • [white]Stake (α_out)[/white] - Total staked alpha tokens across all hotkeys (alpha outstanding) + • [white]Supply (α)[/white] - Circulating alpha token supply + • [white]Tempo (k/n)[/white] - Block interval for subnet updates + + EXAMPLE + + [green]$[/green] btcli subnets list """ self.verbosity_handler(quiet, verbose) - # if (reuse_last or html_output) and self.config.get("use_cache") is False: - # err_console.print( - # "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " - # "Change the config to 'False' using `btcli config set`." - # ) - # raise typer.Exit() - # if reuse_last: - # subtensor = None - # else: subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( @@ -4372,11 +4403,18 @@ def subnets_create( verbose: bool = Options.verbose, ): """ - Registers a new subnet. + Registers a new subnet on the network. - EXAMPLE + This command allows you to create a new subnet and set the subnet's identity. + You also have the option to set your own identity after the registration is complete. + + [bold]Common Examples:[/bold] + 1. Interactive subnet creation: [green]$[/green] btcli subnets create + + 2. Create with GitHub repo and contact email: + [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index f6f161f0..70b02ae8 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -593,7 +593,7 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b safe_staking_description = """ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. - - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" + - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n""" console.print(base_description + (safe_staking_description if safe_staking else "")) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 80d77a6a..1f092ea8 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -1141,6 +1141,6 @@ def _print_table_and_slippage( safe_staking_description = """ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate reduces below this tolerance, the transaction will be limited or rejected. - - [bold white]Partial unstaking[/bold white]: If True, allows unstaking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.""" + - [bold white]Partial unstaking[/bold white]: If True, allows unstaking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n""" console.print(base_description + (safe_staking_description if safe_staking else "")) From 08ed32ab7c6fe97fee618e0c5bcfe93f551d4074 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 11 Feb 2025 20:23:57 -0800 Subject: [PATCH 312/332] Release/9.0.0rc3 --- CHANGELOG.md | 6 ++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41bf79c1..ce288c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 9.0.0rc3 /2025-02-11 + +## What's Changed +* Adds safe staking capability to stake add/remove and related configs +* Other bug fixes and improvements + ## 9.0.0rc2 /2025-02-06 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 7f7f379d..1445e39d 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0rc2" +__version__ = "9.0.0rc3" __all__ = [CLIManager, __version__] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bd364c85..11ff9b50 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -72,7 +72,7 @@ class GitError(Exception): pass -__version__ = "9.0.0rc2" +__version__ = "9.0.0rc3" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 3e93c8dff192c394652b33151b873f92ae11af1f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 12 Feb 2025 23:23:48 +0200 Subject: [PATCH 313/332] Cleanup/ruff --- bittensor_cli/cli.py | 20 ++++------ bittensor_cli/src/__init__.py | 16 +++++++- .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/bittensor/utils.py | 5 ++- bittensor_cli/src/commands/wallets.py | 37 ++++--------------- 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 12cc7753..e0cdc351 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -963,13 +963,13 @@ async def _run(): finally: if initiated is False: asyncio.create_task(cmd).cancel() - if exit_early is True: # temporarily to handle multiple run commands in one session + if ( + exit_early is True + ): # temporarily to handle multiple run commands in one session try: raise typer.Exit() except Exception as e: # ensures we always exit cleanly - if not isinstance( - e, (typer.Exit, RuntimeError) - ): + if not isinstance(e, (typer.Exit, RuntimeError)): err_console.print(f"An unknown error has occurred: {e}") return self.asyncio_runner(_run()) @@ -2209,7 +2209,9 @@ def wallet_new_hotkey( ) if not uri: n_words = get_n_words(n_words) - return self._run_command(wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite)) + return self._run_command( + wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite) + ) def wallet_new_coldkey( self, @@ -2344,13 +2346,7 @@ def wallet_create_wallet( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.wallet_create( - wallet, - n_words, - use_password, - uri, - overwrite - ) + wallets.wallet_create(wallet, n_words, use_password, uri, overwrite) ) def wallet_balance( diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index c85d552d..23a9a114 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -4,7 +4,16 @@ class Constants: - networks = ["local", "finney", "test", "archive", "subvortex", "rao", "dev", "latent-lite"] + networks = [ + "local", + "finney", + "test", + "archive", + "subvortex", + "rao", + "dev", + "latent-lite", + ] finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443" archive_entrypoint = "wss://archive.chain.opentensor.ai:443" @@ -664,7 +673,10 @@ class WalletValidationTypes(Enum): "alpha_values": ("sudo_set_alpha_values", False), "liquid_alpha_enabled": ("sudo_set_liquid_alpha_enabled", False), "network_registration_allowed": ("sudo_set_network_registration_allowed", False), - "network_pow_registration_allowed": ("sudo_set_network_pow_registration_allowed", False) + "network_pow_registration_allowed": ( + "sudo_set_network_pow_registration_allowed", + False, + ), } # Help Panels for cli help diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7d77d9a6..cf5ac9e9 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -30,7 +30,7 @@ err_console, decode_hex_identity_dict, validate_chain_endpoint, - u16_normalized_float + u16_normalized_float, ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 59fa286d..cbd27de6 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1293,11 +1293,14 @@ def is_linux(): """Returns True if the operating system is Linux.""" return platform.system().lower() == "linux" + def validate_rate_tolerance(value: Optional[float]) -> Optional[float]: """Validates rate tolerance input""" if value is not None: if value < 0: - raise typer.BadParameter("Rate tolerance cannot be negative (less than 0%).") + raise typer.BadParameter( + "Rate tolerance cannot be negative (less than 0%)." + ) if value > 1: raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).") if value > 0.5: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 90bec6a3..eab80891 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -44,7 +44,6 @@ get_subnet_name, millify_tao, unlock_key, - hex_to_bytes, WalletLike, ) @@ -1076,12 +1075,11 @@ async def transfer( ): """Transfer token of amount to destination.""" await transfer_extrinsic( - # TODO verify the order here - subtensor, - wallet, - destination, - Balance.from_tao(amount), - transfer_all, + subtensor=subtensor, + wallet=wallet, + destination=destination, + amount=Balance.from_tao(amount), + transfer_all=transfer_all, prompt=prompt, ) @@ -1335,11 +1333,7 @@ async def set_id( ) return False - try: - # TODO unlock fn - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False call = await subtensor.substrate.compose_call( @@ -1431,30 +1425,15 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): async def sign(wallet: Wallet, message: str, use_hotkey: str): """Sign a message using the provided wallet or hotkey.""" - def _unlock(key: str): - try: - getattr(wallet, f"unlock_{key}")() - return True - except PasswordError: - err_console.print( - ":cross_mark: [red]The password used to decrypt your keyfile is invalid[/red]" - ) - return False - except KeyFileError: - err_console.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, or non-readable[/red]:" - ) - return False - if not use_hotkey: - if not _unlock("coldkey"): + if not unlock_key(wallet, "coldkey").success: return False keypair = wallet.coldkey print_verbose( f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}" ) else: - if not _unlock("hotkey"): + if not unlock_key(wallet, "hotkey").success: return False keypair = wallet.hotkey print_verbose( From 202c62f55af62e493557ac4fecfc2430d96e3205 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 15:47:41 -0800 Subject: [PATCH 314/332] Adds fixes, bumps changelog and version --- CHANGELOG.md | 5 ++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 15 +++- bittensor_cli/src/commands/subnets/subnets.py | 77 ++++++++----------- 4 files changed, 48 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce288c5e..3dd2302d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 9.0.0rc4 /2025-02-12 +* Adds sort option to metagraph/show +* Updates metagraph/show to use direct dividends instead of relative +* Updates subnets list to use working tao emissions + ## 9.0.0rc3 /2025-02-11 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 1445e39d..59ae1b73 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0rc3" +__version__ = "9.0.0rc4" __all__ = [CLIManager, __version__] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 11ff9b50..2efb3b5f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -72,7 +72,7 @@ class GitError(Exception): pass -__version__ = "9.0.0rc3" +__version__ = "9.0.0rc4" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -2919,6 +2919,7 @@ def stake_add( subnets.show( subtensor=self.initialize_chain(network), netuid=netuid, + sort=False, max_rows=12, prompt=False, delegate_selection=True, @@ -4327,6 +4328,11 @@ def subnets_show( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + sort: bool = typer.Option( + False, + "--sort", + help="Sort the subnets by uid.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -4342,8 +4348,11 @@ def subnets_show( subtensor = self.initialize_chain(network) return self._run_command( subnets.show( - subtensor, - netuid, + subtensor=subtensor, + netuid=netuid, + sort=sort, + max_rows=None, + delegate_selection=False, verbose=verbose, prompt=prompt, ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 59003eb1..e19aa6f0 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -326,7 +326,7 @@ def create_table(subnets, block_number): if netuid == 0: emission_tao = 0.0 else: - emission_tao = subnet.emission.tao + emission_tao = subnet.tao_in_emission.tao alpha_in_value = ( f"{millify_tao(subnet.alpha_in.tao)}" @@ -399,7 +399,11 @@ def create_table(subnets, block_number): ) total_emissions = round( - sum(float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0), + sum( + float(subnet.tao_in_emission) + for subnet in subnets + if subnet.netuid != 0 + ), 4, ) total_rate = round( @@ -528,7 +532,7 @@ def format_liquidity_cell( if netuid == 0: emission_tao = 0.0 else: - emission_tao = subnet.emission.tao + emission_tao = subnet.tao_in_emission.tao market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao supply = subnet.alpha_in.tao + subnet.alpha_out.tao @@ -804,6 +808,7 @@ def format_liquidity_cell( async def show( subtensor: "SubtensorInterface", netuid: int, + sort: bool = False, max_rows: Optional[int] = None, delegate_selection: bool = False, verbose: bool = False, @@ -850,18 +855,6 @@ async def show_root(): ) table.add_column("[bold white]Position", style="white", justify="center") - # table.add_column( - # f"[bold white]Total Stake ({Balance.get_unit(0)})", - # style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], - # justify="center", - # ) - # ------- Temporary columns for testing ------- - # table.add_column( - # "Alpha (τ)", - # style=COLOR_PALETTE["POOLS"]["EXTRA_2"], - # no_wrap=True, - # justify="right", - # ) table.add_column( "Tao (τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], @@ -869,7 +862,6 @@ async def show_root(): justify="right", footer=f"{tao_sum:.4f} τ" if verbose else f"{millify_tao(tao_sum)} τ", ) - # ------- End Temporary columns for testing ------- table.add_column( f"[bold white]Emission ({Balance.get_unit(0)}/block)", style=COLOR_PALETTE["POOLS"]["EMISSION"], @@ -1102,7 +1094,10 @@ async def show_subnet(netuid_: int): for idx in range(len(subnet_state.tao_stake)) ] ) - relative_emissions_sum = 0 + dividends_sum = sum( + subnet_state.dividends[idx] for idx in range(len(subnet_state.dividends)) + ) + owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) if subnet_info.owner_hotkey not in owner_hotkeys: owner_hotkeys.append(subnet_info.owner_hotkey) @@ -1118,25 +1113,24 @@ async def show_subnet(netuid_: int): sorted_indices = sorted( range(len(subnet_state.hotkeys)), key=lambda i: ( - # Sort by owner status first - not ( - subnet_state.coldkeys[i] == subnet_info.owner_coldkey - or subnet_state.hotkeys[i] in owner_hotkeys - ), - # Then sort by stake amount (higher stakes first) - -subnet_state.total_stake[i].tao, + # If sort is True, sort only by UIDs + i + if sort + else ( + # Otherwise + # Sort by owner status first + not ( + subnet_state.coldkeys[i] == subnet_info.owner_coldkey + or subnet_state.hotkeys[i] in owner_hotkeys + ), + # Then sort by stake amount (higher stakes first) + -subnet_state.total_stake[i].tao, + ) ), ) rows = [] for idx in sorted_indices: - hotkey_block_emission = ( - subnet_state.emission[idx].tao / emission_sum - if emission_sum != 0 - else 0 - ) - relative_emissions_sum += hotkey_block_emission - # Get identity for this uid coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( "name", "" @@ -1175,11 +1169,11 @@ async def show_subnet(netuid_: int): f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake - # str(subnet_state.dividends[idx]), - f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends - f"{subnet_state.incentives[idx]:.4f}", # Incentive + f"{subnet_state.dividends[idx]:.6f}", # Dividends + # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends + f"{subnet_state.incentives[idx]:.6f}", # Incentive # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative - f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions + f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions f"{subnet_state.hotkeys[idx][:6]}" if not verbose else f"{subnet_state.hotkeys[idx]}", # Hotkey @@ -1201,7 +1195,6 @@ async def show_subnet(netuid_: int): if verbose else f"{millify_tao(stake_sum)} {subnet_info.symbol}", ) - # ------- Temporary columns for testing ------- table.add_column( f"Alpha ({Balance.get_unit(netuid_)})", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], @@ -1220,24 +1213,14 @@ async def show_subnet(netuid_: int): if verbose else f"{millify_tao(tao_sum)} {subnet_info.symbol}", ) - # ------- End Temporary columns for testing ------- table.add_column( "Dividends", style=COLOR_PALETTE["POOLS"]["EMISSION"], no_wrap=True, justify="center", - footer=f"{relative_emissions_sum:.3f}", + footer=f"{dividends_sum:.3f}", ) table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") - - # Hiding relative emissions for now - # table.add_column( - # "Emissions", - # style="light_goldenrod2", - # no_wrap=True, - # justify="center", - # footer=f"{relative_emissions_sum:.3f}", - # ) table.add_column( f"Emissions ({Balance.get_unit(netuid_)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], From 5231d7e7548e94214c5dbaffafa583e847ec7dc2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 15:53:41 -0800 Subject: [PATCH 315/332] update sum --- bittensor_cli/src/commands/subnets/subnets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index e19aa6f0..fe62a365 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -400,7 +400,7 @@ def create_table(subnets, block_number): total_emissions = round( sum( - float(subnet.tao_in_emission) + subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0 ), @@ -661,7 +661,7 @@ def format_liquidity_cell( # Calculate totals total_netuids = len(subnets) _total_emissions = sum( - float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 + float(subnet.tao_in_emission.tao) for subnet in subnets if subnet.netuid != 0 ) total_emissions = ( f"{millify_tao(_total_emissions)}" @@ -670,7 +670,7 @@ def format_liquidity_cell( ) total_rate = sum( - float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + subnet.price.tao for subnet in subnets if subnet.netuid != 0 ) total_rate = ( f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" From e20a4ed2c9b8f32a09dc4b4a5f4d9dc43e3628ce Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 15:54:38 -0800 Subject: [PATCH 316/332] remove float --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index fe62a365..2778776c 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -661,7 +661,7 @@ def format_liquidity_cell( # Calculate totals total_netuids = len(subnets) _total_emissions = sum( - float(subnet.tao_in_emission.tao) for subnet in subnets if subnet.netuid != 0 + subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0 ) total_emissions = ( f"{millify_tao(_total_emissions)}" From 9de2e15ed1d82586caeeabb339efbb527ad7107d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 15:59:33 -0800 Subject: [PATCH 317/332] Cleanup --- bittensor_cli/src/commands/subnets/subnets.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2778776c..44eea5e5 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -399,11 +399,7 @@ def create_table(subnets, block_number): ) total_emissions = round( - sum( - subnet.tao_in_emission.tao - for subnet in subnets - if subnet.netuid != 0 - ), + sum(subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0), 4, ) total_rate = round( @@ -669,9 +665,7 @@ def format_liquidity_cell( else f"{_total_emissions:,.2f}" ) - total_rate = sum( - subnet.price.tao for subnet in subnets if subnet.netuid != 0 - ) + total_rate = sum(subnet.price.tao for subnet in subnets if subnet.netuid != 0) total_rate = ( f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" ) @@ -1067,14 +1061,6 @@ async def show_subnet(netuid_: int): pad_edge=True, ) - # For hotkey_block_emission calculation - emission_sum = sum( - [ - subnet_state.emission[idx].tao - for idx in range(len(subnet_state.emission)) - ] - ) - # For table footers alpha_sum = sum( [ @@ -1097,6 +1083,12 @@ async def show_subnet(netuid_: int): dividends_sum = sum( subnet_state.dividends[idx] for idx in range(len(subnet_state.dividends)) ) + emission_sum = sum( + [ + subnet_state.emission[idx].tao + for idx in range(len(subnet_state.emission)) + ] + ) owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) if subnet_info.owner_hotkey not in owner_hotkeys: @@ -1170,9 +1162,7 @@ async def show_subnet(netuid_: int): if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake f"{subnet_state.dividends[idx]:.6f}", # Dividends - # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends f"{subnet_state.incentives[idx]:.6f}", # Incentive - # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions f"{subnet_state.hotkeys[idx][:6]}" if not verbose From 73328df3f517aa55bbed4257aab330a6de55ebb5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 17:17:14 -0800 Subject: [PATCH 318/332] Fix merge artifacts --- bittensor_cli/cli.py | 33 ++++++++++++++++++++------------- tests/e2e_tests/conftest.py | 2 +- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e0cdc351..23784740 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2884,7 +2884,7 @@ def stake_add( ) raise typer.Exit() - if not stake_all and not amount and not max_stake: + if not stake_all and not amount: amount = FloatPrompt.ask("Amount to [blue]stake (TAO τ)[/blue]") if stake_all and not amount: @@ -2892,14 +2892,14 @@ def stake_add( raise typer.Exit() if all_hotkeys and include_hotkeys: - err_console.print( + print_error( "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." ) raise typer.Exit() if include_hotkeys and exclude_hotkeys: - err_console.print( + print_error( "You have specified options for both including and excluding hotkeys. Select one or the other." ) raise typer.Exit() @@ -2974,6 +2974,8 @@ def stake_add( "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + include_hotkeys = [] if exclude_hotkeys: exclude_hotkeys = parse_to_list( @@ -2983,7 +2985,7 @@ def stake_add( is_ss58=True, ) else: - excluded_hotkeys = [] + exclude_hotkeys = [] # TODO: Ask amount for each subnet explicitly if more than one if not stake_all and not amount: @@ -3023,8 +3025,8 @@ def stake_add( amount, prompt, all_hotkeys, - included_hotkeys, - excluded_hotkeys, + include_hotkeys, + exclude_hotkeys, safe_staking, rate_tolerance, allow_partial_stake, @@ -3132,32 +3134,32 @@ def stake_remove( if interactive and any( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] ): - err_console.print( + print_error( "Interactive mode cannot be used with hotkey selection options like --include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) raise typer.Exit() if unstake_all and unstake_all_alpha: - err_console.print("Cannot specify both unstake-all and unstake-all-alpha.") + print_error("Cannot specify both unstake-all and unstake-all-alpha.") raise typer.Exit() if not interactive and not unstake_all and not unstake_all_alpha: netuid = get_optional_netuid(netuid, all_netuids) if all_hotkeys and include_hotkeys: - err_console.print( + print_error( "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" " should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." ) raise typer.Exit() if include_hotkeys and exclude_hotkeys: - err_console.print( + print_error( "You have specified both including and excluding hotkeys options. Select one or the other." ) raise typer.Exit() if unstake_all and amount: - err_console.print( + print_error( "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." ) raise typer.Exit() @@ -3292,8 +3294,8 @@ def stake_remove( subtensor=self.initialize_chain(network), hotkey_ss58_address=hotkey_ss58_address, all_hotkeys=all_hotkeys, - include_hotkeys=included_hotkeys, - exclude_hotkeys=excluded_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, amount=amount, prompt=prompt, interactive=interactive, @@ -3976,6 +3978,11 @@ def sudo_set( """ self.verbosity_handler(quiet, verbose) + hyperparams = self._run_command( + sudo.get_hyperparameters(self.initialize_chain(network), netuid), + exit_early=False, + ) + if not hyperparams: raise typer.Exit() if not param_name or not param_value: diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index aaf8dde8..4d9f9c7b 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -18,7 +18,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" + script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: # Skip the test if the localhost.sh path is not set From 23cf893f07cbab01806075e5a08c43db52934fd5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 17:28:48 -0800 Subject: [PATCH 319/332] fix wallet transfer all --- bittensor_cli/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 23784740..4f37b58d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1556,7 +1556,7 @@ def wallet_list( """ Displays all the wallets and their corresponding hotkeys that are located in the wallet path specified in the config. - The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output is presented in a hierarchical tree format, with each wallet as a root node and any associated hotkeys as child nodes. The `ss58` address (or an `` marker, for encrypted hotkeys) is displayed for each coldkey and hotkey that exists on the device. + The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output is presented in a hierarchical tree format, with each wallet as a root node and any associated hotkeys as child nodes. The `ss58` address (or an `` marker, for encrypted hotkeys) is displayed for each coldkey and hotkey that exists on the device. Upon invocation, the command scans the wallet directory and prints a list of all the wallets, indicating whether the public keys are available (`?` denotes unavailable or encrypted keys). @@ -1689,7 +1689,6 @@ def wallet_transfer( None, "--amount", "-a", - prompt=True, help="Amount (in TAO) to transfer.", ), transfer_all: bool = typer.Option( From 20c496a9720702b5edd74c462b823d5aa167c1ae Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 17:34:50 -0800 Subject: [PATCH 320/332] fix set_id and dupe hyperparam printing --- bittensor_cli/cli.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4f37b58d..7bd29be4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2575,8 +2575,6 @@ def wallet_set_id( "Current on-chain identity", ), exit_early=False, - ask_for=[WO.HOTKEY, WO.NAME], - validate=WV.WALLET_AND_HOTKEY, ) if prompt: @@ -3977,13 +3975,6 @@ def sudo_set( """ self.verbosity_handler(quiet, verbose) - hyperparams = self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid), - exit_early=False, - ) - - if not hyperparams: - raise typer.Exit() if not param_name or not param_value: hyperparams = self._run_command( sudo.get_hyperparameters(self.initialize_chain(network), netuid), From 8500d4e17cae8e96dc0bce976a0e41c68ec28ff9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 17:59:04 -0800 Subject: [PATCH 321/332] updates unlocking in registration extrinsic --- bittensor_cli/__init__.py | 2 +- bittensor_cli/src/bittensor/extrinsics/registration.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 1445e39d..e8cc8713 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -20,4 +20,4 @@ __version__ = "9.0.0rc3" -__all__ = [CLIManager, __version__] +__all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 5bd791ec..7c747ad8 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -693,12 +693,9 @@ async def burned_register_extrinsic( :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ - - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False + + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + return False, unlock_status.message with console.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", From 5fcd2cee880db454ae0600190c094aaa69e43b0c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 22:31:25 -0800 Subject: [PATCH 322/332] Updates e2e tests for rao --- bittensor_cli/src/commands/wallets.py | 4 +- tests/e2e_tests/test_staking_sudo.py | 32 ++- tests/e2e_tests/test_wallet_interactions.py | 230 +++++++++----------- tests/e2e_tests/utils.py | 43 +--- 4 files changed, 136 insertions(+), 173 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index eab80891..4af28d4e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1426,14 +1426,14 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): """Sign a message using the provided wallet or hotkey.""" if not use_hotkey: - if not unlock_key(wallet, "coldkey").success: + if not unlock_key(wallet, "cold").success: return False keypair = wallet.coldkey print_verbose( f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}" ) else: - if not unlock_key(wallet, "hotkey").success: + if not unlock_key(wallet, "hot").success: return False keypair = wallet.hotkey print_verbose( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index abb3fe5d..6684b1a1 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -47,19 +47,27 @@ def test_staking(local_chain, wallet_setup): extra_args=[ "--wallet-path", wallet_path_alice, - "--wallet-hotkey", - wallet_alice.hotkey_str, "--chain", "ws://127.0.0.1:9945", "--wallet-name", wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Created by Alice", "--no-prompt", - "--subnet-name", - "test-subnet", - "--github-repo", - "https://github.com/bittensor/bittensor", - "--subnet-contact", - "test@test.com", ], ) assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout @@ -101,6 +109,9 @@ def test_staking(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--amount", "100", + "--tolerance", + "0.1", + "--partial", "--no-prompt", ], ) @@ -123,7 +134,7 @@ def test_staking(local_chain, wallet_setup): cleaned_stake = [ re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines() ] - stake_added = cleaned_stake[9].split()[8] + stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(100) # Execute remove_stake command and remove all 100 TAO from Alice @@ -143,6 +154,9 @@ def test_staking(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--amount", "100", + "--tolerance", + "0.1", + "--partial", "--no-prompt", ], ) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index c43efb90..ad44c5a1 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -38,7 +38,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): AssertionError: If any of the checks or verifications fail """ print("Testing wallet overview, inspect command 🧪") - netuid = 1 + netuid = 2 wallet_path_name = "//Alice" # Create wallet for Alice @@ -55,6 +55,22 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--wallet-name", wallet.name, + "--wallet-hotkey", + wallet.hotkey_str, + "--name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Test subnet", "--no-prompt", ], ) @@ -77,26 +93,6 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Assert using regex that the subnet is visible in subnets list assert verify_subnet_entry(subnets_list.stdout, netuid, keypair.ss58_address) - # Register Alice in netuid = 1 using her hotkey - register_subnet = exec_command( - command="subnets", - sub_command="register", - extra_args=[ - "--wallet-path", - wallet_path, - "--wallet-name", - wallet.name, - "--hotkey", - wallet.hotkey_str, - "--netuid", - "1", - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - ], - ) - assert "✅ Registered" in register_subnet.stdout - # Check balance of Alice after registering to the subnet wallet_balance = exec_command( command="wallet", @@ -145,31 +141,33 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): axon_active=False, # Axon is not active until we run validator/miner ) - # Execute wallet inspect command - inspect = exec_command( - command="wallet", - sub_command="inspect", - extra_args=[ - "--wallet-path", - wallet_path, - "--wallet-name", - wallet.name, - "--chain", - "ws://127.0.0.1:9945", - ], - ) - - # Assert correct entry is present in wallet inspect - assert validate_wallet_inspect( - inspect.stdout, - coldkey=wallet.name, - balance=Balance.from_tao(balance["free_balance"]), - delegates=None, # We have not delegated anywhere yet - hotkeys_netuid=[ - (1, f"default-{wallet.hotkey.ss58_address}", 0, False) - ], # (netuid, hotkey-display, stake, check_emissions) - ) - print("Passed wallet overview, inspect command ✅") + # TODO: Re-enable this once inspect is ported over + if False: + # Execute wallet inspect command + inspect = exec_command( + command="wallet", + sub_command="inspect", + extra_args=[ + "--wallet-path", + wallet_path, + "--wallet-name", + wallet.name, + "--chain", + "ws://127.0.0.1:9945", + ], + ) + + # Assert correct entry is present in wallet inspect + assert validate_wallet_inspect( + inspect.stdout, + coldkey=wallet.name, + balance=Balance.from_tao(balance["free_balance"]), + delegates=None, # We have not delegated anywhere yet + hotkeys_netuid=[ + (1, f"default-{wallet.hotkey.ss58_address}", 0, False) + ], # (netuid, hotkey-display, stake, check_emissions) + ) + print("Passed wallet overview command ✅") def test_wallet_transfer(local_chain, wallet_setup): @@ -353,7 +351,7 @@ def test_wallet_identities(local_chain, wallet_setup): """ print("Testing wallet set-id, get-id, sign command 🧪") - netuid = 1 + netuid = 2 wallet_path_alice = "//Alice" # Create wallet for Alice @@ -361,25 +359,6 @@ def test_wallet_identities(local_chain, wallet_setup): wallet_path_alice ) - # Register Alice to the root network (0) - # Either root list neurons + subnet registered can set-id or subnet owners - root_register = exec_command_alice( - command="root", - sub_command="register", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--network", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--no-prompt", - ], - ) - assert "✅ Registered" in root_register.stdout - # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", @@ -391,42 +370,36 @@ def test_wallet_identities(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--wallet-name", wallet_alice.name, - "--no-prompt", - ], - ) - assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout - - # Register Alice in netuid = 1 using her hotkey - register_subnet = exec_command_alice( - command="subnets", - sub_command="register", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", + "--wallet-hotkey", wallet_alice.hotkey_str, - "--netuid", - netuid, - "--chain", - "ws://127.0.0.1:9945", + "--name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Created by Alice", "--no-prompt", ], ) - assert "✅ Registered" in register_subnet.stdout + assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout # Define values for Alice's identity alice_identity = { - "display_name": "Alice", - "legal_name": "Alice OTF", - "web_url": "https://bittensor.com/", - "riot": "MyRiotID", - "email": "alice@opentensor.dev", - "pgp": "D2A1 F4A3 B1D3 5A74 63F0 678E 35E7 041A 22C1 A4FE", - "image_url": "https://bittensor.com/img/dark-Bittensor.svg", - "info": "I am a tester for OTF", - "twitter": "https://x.com/opentensor", + "name": "Alice OTF", + "url": "https://bittensor.com/", + "image": "https://bittensor.com/img/dark-Bittensor.svg", + "discord": "alice#1234", + "description": "I am a tester for OTF", + "additional": "Lead Developer", + "github_repo": "https://github.com/opentensor/bittensor", } # Execute btcli set-identity command @@ -442,25 +415,20 @@ def test_wallet_identities(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--display-name", - alice_identity["display_name"], - "--legal-name", - alice_identity["legal_name"], + "--name", + alice_identity["name"], "--web-url", - alice_identity["web_url"], - "--riot", - alice_identity["riot"], - "--email", - alice_identity["email"], - "--pgp", - alice_identity["pgp"], - "--image-url", - alice_identity["image_url"], - "--info", - alice_identity["info"], - "-x", - alice_identity["twitter"], - "--validator", + alice_identity["url"], + "--image-url", + alice_identity["image"], + "--discord", + alice_identity["discord"], + "--description", + alice_identity["description"], + "--additional", + alice_identity["additional"], + "--github", + alice_identity["github_repo"], "--no-prompt", ], ) @@ -469,18 +437,18 @@ def test_wallet_identities(local_chain, wallet_setup): assert "✅ Success!" in set_id.stdout set_id_output = set_id.stdout.splitlines() - assert alice_identity["display_name"] in set_id_output[7] - assert alice_identity["legal_name"] in set_id_output[8] - assert alice_identity["web_url"] in set_id_output[9] - assert alice_identity["riot"] in set_id_output[10] - assert alice_identity["email"] in set_id_output[11] - assert alice_identity["pgp"] in set_id_output[12] - assert alice_identity["image_url"] in set_id_output[13] - assert alice_identity["twitter"] in set_id_output[14] + assert alice_identity["name"] in set_id_output[6] + assert alice_identity["url"] in set_id_output[7] + assert alice_identity["github_repo"] in set_id_output[8] + assert alice_identity["image"] in set_id_output[9] + assert alice_identity["discord"] in set_id_output[10] + assert alice_identity["description"] in set_id_output[11] + assert alice_identity["additional"] in set_id_output[12] + # TODO: Currently coldkey + hotkey are the same for test wallets. # Maybe we can add a new key to help in distinguishing - assert wallet_alice.hotkey.ss58_address in set_id_output[5] + assert wallet_alice.coldkeypub.ss58_address in set_id_output[5] # Execute btcli get-identity using hotkey get_identity = exec_command_alice( @@ -490,20 +458,20 @@ def test_wallet_identities(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--key", - wallet_alice.hotkey.ss58_address, + wallet_alice.coldkeypub.ss58_address, ], ) # Assert all correct values are being fetched for the ID we just set get_identity_output = get_identity.stdout.splitlines() - assert alice_identity["display_name"] in get_identity_output[6] - assert alice_identity["legal_name"] in get_identity_output[7] - assert alice_identity["web_url"] in get_identity_output[8] - assert alice_identity["riot"] in get_identity_output[9] - assert alice_identity["email"] in get_identity_output[10] - assert alice_identity["pgp"] in get_identity_output[11] - assert alice_identity["image_url"] in get_identity_output[12] - assert alice_identity["twitter"] in get_identity_output[13] + assert alice_identity["name"] in get_identity_output[5] + assert alice_identity["url"] in get_identity_output[6] + assert alice_identity["github_repo"] in get_identity_output[7] + assert alice_identity["image"] in get_identity_output[8] + assert alice_identity["discord"] in get_identity_output[9] + assert alice_identity["description"] in get_identity_output[10] + assert alice_identity["additional"] in get_identity_output[11] + # Sign a message using hotkey sign_using_hotkey = exec_command_alice( diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index ebad8ff4..a568af51 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -74,9 +74,7 @@ def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) - """ pattern = ( rf"{wallet_name}\s+{coldkey_address}\s+" - r"τ([\d,]+\.\d+)\s+" # Free Balance - r"τ([\d,]+\.\d+)\s+" # Staked Balance - r"τ([\d,]+\.\d+)" # Total Balance + r"τ\s*([\d,]+\.\d+)" # Free Balance ) match = re.search(pattern, text) @@ -84,15 +82,11 @@ def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) - if not match: return { "free_balance": 0.0, - "staked_balance": 0.0, - "total_balance": 0.0, } # Return the balances as a dictionary return { "free_balance": float(match.group(1).replace(",", "")), - "staked_balance": float(match.group(2).replace(",", "")), - "total_balance": float(match.group(3).replace(",", "")), } @@ -109,24 +103,11 @@ def verify_subnet_entry(output_text: str, netuid: str, ss58_address: str) -> boo bool: True if the entry is found, False otherwise. """ - pattern = ( - rf"\b{re.escape(str(netuid))}\s*\│\s*" # NETUID - r"\d+\s*\│\s*" # N (any number) - r"\d+(?:\.\d+)?\s*[KMB]*\s*\│\s*" # MAX_N (number with optional decimal and K/M/B suffix) - r"\d+\.\d+%\s*\│\s*" # EMISSION (percentage) - r"\d+\s*\│\s*" # TEMPO (any number) - r"τ\d+\.\d+\s*\│\s*" # RECYCLE (τ followed by a number) - r"\d+(?:\.\d+)?\s*[KMB]*\s*\│\s*" # POW (number with optional decimal and K/M/B suffix) - rf"{re.escape(ss58_address)}\b" # SUDO (exact SS58 address) - ) - - # Normalize spaces in the output text - normalized_output = re.sub(r"\s+", " ", output_text) - - # Search for the pattern - match = re.search(pattern, normalized_output) - - return bool(match) + pattern = rf"^\s*{re.escape(str(netuid))}\s*[│┃]" + for line in output_text.splitlines(): + if re.search(pattern, line): + return True + return False def validate_wallet_overview( @@ -148,20 +129,20 @@ def validate_wallet_overview( pattern = rf"{coldkey}\s+" # COLDKEY pattern += rf"{hotkey}\s+" # HOTKEY pattern += rf"{uid}\s+" # UID - pattern += r"True\s+" # ACTIVE - Always True immediately after we register - pattern += r"[\d.]+\s+" # STAKE(τ) + pattern += r"True\s+" # ACTIVE + pattern += r"[\d.]+\s+" # STAKE pattern += r"[\d.]+\s+" # RANK pattern += r"[\d.]+\s+" # TRUST pattern += r"[\d.]+\s+" # CONSENSUS pattern += r"[\d.]+\s+" # INCENTIVE pattern += r"[\d.]+\s+" # DIVIDENDS - pattern += r"\d+\s+" # EMISSION(ρ) + pattern += r"[\d.]+\s+" # EMISSION pattern += r"[\d.]+\s+" # VTRUST - pattern += r"(?:True|False)?\s*" # VPERMIT (optional) - pattern += r"[\d]+\s+" # UPDATED (any number) + pattern += r"\*?\s*" # VPERMIT (optional *) + pattern += r"[\d]+\s+" # UPDATED pattern += ( r"(?!none)\w+\s+" if axon_active else r"none\s+" - ) # AXON - True if axon is active + ) # AXON pattern += rf"{hotkey_ss58[:10]}\s*" # HOTKEY_SS58 # Search for the pattern in the wallet information From e64b3811e57cb7c4e84979fdf3c25fb7cbbb523c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 22:44:18 -0800 Subject: [PATCH 323/332] Updates subtensor branch --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index ab7f7145..1c625180 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Setup subtensor repo working-directory: ${{ github.workspace }}/subtensor - run: git checkout main + run: git checkout testnet - name: Install Python dependencies run: python3 -m pip install -e . pytest From 1314e1b8ac0e1a20d2a87816ca516792e065c5b1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 12 Feb 2025 23:58:20 -0800 Subject: [PATCH 324/332] Fixes stake amount prompt --- bittensor_cli/cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3ac1ffe2..5c09924a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2881,9 +2881,6 @@ def stake_add( ) raise typer.Exit() - if not stake_all and not amount: - amount = FloatPrompt.ask("Amount to [blue]stake (TAO τ)[/blue]") - if stake_all and not amount: if not Confirm.ask("Stake all the available TAO tokens?", default=False): raise typer.Exit() From 0999b1f36e3866d2c0a36f21c1e5618cd8eea9bf Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 13 Feb 2025 16:30:35 +0200 Subject: [PATCH 325/332] Tidying. --- .../src/bittensor/extrinsics/registration.py | 2 +- bittensor_cli/src/bittensor/utils.py | 1 - bittensor_cli/src/commands/subnets/subnets.py | 2 +- bittensor_cli/src/commands/sudo.py | 16 ++++++++-------- bittensor_cli/src/commands/wallets.py | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 7c747ad8..65d8f348 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -693,7 +693,7 @@ async def burned_register_extrinsic( :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ - + if not (unlock_status := unlock_key(wallet, print_out=False)).success: return False, unlock_status.message diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index cbd27de6..2ac4019e 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -32,7 +32,6 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters - from async_substrate_interface.async_substrate import AsyncSubstrateInterface console = Console() err_console = Console(stderr=True) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 44eea5e5..141dfb62 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1322,7 +1322,7 @@ async def show_subnet(netuid_: int): return hotkey else: console.print( - f"[red]Invalid UID. Please enter a valid UID from the table above[/red]" + "[red]Invalid UID. Please enter a valid UID from the table above[/red]" ) except ValueError: console.print("[red]Please enter a valid number[/red]") diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index bf850b40..2285a0bd 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -657,10 +657,10 @@ async def senate_vote( return False # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: + if ( + not unlock_key(wallet, "hotkey").success + and unlock_key(wallet, "coldkey").success + ): return False console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") @@ -736,10 +736,10 @@ async def _do_set_take() -> bool: f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}" ) - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: + if ( + not unlock_key(wallet, "hotkey").success + and unlock_key(wallet, "coldkey").success + ): return False result_ = await _do_set_take() diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4af28d4e..e3a3e8d6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -6,7 +6,7 @@ import aiohttp from bittensor_wallet import Wallet, Keypair -from bittensor_wallet.errors import KeyFileError, PasswordError +from bittensor_wallet.errors import KeyFileError from bittensor_wallet.keyfile import Keyfile from fuzzywuzzy import fuzz from rich import box From 2c2e690590ec1d5088f74fd9442e3627f9e76271 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 09:21:05 -0800 Subject: [PATCH 326/332] Fix dupe -p flag --- bittensor_cli/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5c09924a..cd3356f2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3744,7 +3744,6 @@ def stake_set_children( [], "--proportions", "--prop", - "-p", help="Enter the stake weight proportions for the child hotkeys (sum should be less than or equal to 1)", prompt=False, ), From da94e97c1ee057b4643c22815633d5b0044f01c3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 09:28:08 -0800 Subject: [PATCH 327/332] Updates tao_weight for mainnet --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 141dfb62..4f116e62 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -40,7 +40,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -TAO_WEIGHT = 0.018 +TAO_WEIGHT = 0.18 # helpers and extrinsics From 380f0a0fd69b1902a9c7b3dafe38edbeaadcbf35 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 09:56:37 -0800 Subject: [PATCH 328/332] fix unlocking in sudo --- bittensor_cli/src/commands/sudo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 2285a0bd..58c6a4b1 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -658,8 +658,8 @@ async def senate_vote( # Unlock the wallet. if ( - not unlock_key(wallet, "hotkey").success - and unlock_key(wallet, "coldkey").success + not unlock_key(wallet, "hot").success + and unlock_key(wallet, "cold").success ): return False @@ -737,8 +737,8 @@ async def _do_set_take() -> bool: ) if ( - not unlock_key(wallet, "hotkey").success - and unlock_key(wallet, "coldkey").success + not unlock_key(wallet, "hot").success + and unlock_key(wallet, "cold").success ): return False From 25d52facc54380321f6248e406bb1125dd3dcbb0 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 10:48:11 -0800 Subject: [PATCH 329/332] Bumps version and changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a7ffbba..07d88df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 9.0.0 /2025-02-13 + +## What's Changed +* fix netuid from str to int by @roman-opentensor in https://github.com/opentensor/btcli/pull/195 +* add runtime apis to reg by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/196 +* Updated tables (st list, s list) by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/200 +* Modifying descriptions and links in stake and subnets dot py files by @rajkaramchedu in https://github.com/opentensor/btcli/pull/246 +* Fixes Identity Lookup (Rao Games Pools) by @thewhaleking in https://github.com/opentensor/btcli/pull/279 +* Show encrypted hotkeys in w list by @thewhaleking in https://github.com/opentensor/btcli/pull/288 +* Backmerge rao branch to decoding branch by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/290 +* Updates identity, sn identity, and other chain stuff by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/292 +* Updates Rao to decode using chain by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/286 +* Fix/rao remove mention of cost by @camfairchild in https://github.com/opentensor/btcli/pull/293 +* Uses uvloop if it's installed by @thewhaleking in https://github.com/opentensor/btcli/pull/294 +* Feat: Safe staking by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/299 +* Removes stake from w balances by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/301 +* Updates docstrings for commands by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/303 +* Release/9.0.0rc4 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/306 +* Rao to staging merge (new branch) by @thewhaleking in https://github.com/opentensor/btcli/pull/305 +* [WIP] Rao by @thewhaleking in https://github.com/opentensor/btcli/pull/129 +* Updates e2e tests for rao by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/307 +* Update dividends, adds sort by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/308 +* Final cleanups for Rao by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/309 + +## New Contributors +* @camfairchild made their first contribution in https://github.com/opentensor/btcli/pull/293 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v8.4.3...v9.0.0 + ## 9.0.0rc4 /2025-02-12 * Adds sort option to metagraph/show * Updates metagraph/show to use direct dividends instead of relative diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index b8718f44..55ce167e 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0rc4" +__version__ = "9.0.0" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cd3356f2..142a9402 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -72,7 +72,7 @@ class GitError(Exception): pass -__version__ = "9.0.0rc4" +__version__ = "9.0.0" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) From 91303d8fa871b2ef7b94096615991e2ab7ea1bda Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 10:50:22 -0800 Subject: [PATCH 330/332] Bumps deps --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b9f56c3a..521e9494 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface>=1.0.0rc14 +async-substrate-interface>=1.0.0 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 @@ -16,7 +16,7 @@ rich~=13.7 scalecodec==1.2.11 typer~=0.12 websockets>=14.1 -bittensor-wallet>=3.0.2 +bittensor-wallet>=3.0.3 plotille pywry plotly \ No newline at end of file From eac378be586df22799a2bb29641dda55c3ab6855 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 11:02:16 -0800 Subject: [PATCH 331/332] Updates type --- bittensor_cli/src/commands/sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 56f49710..dbbdf537 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -585,7 +585,7 @@ async def get_senate(subtensor: "SubtensorInterface"): return console.print(table) -async def proposals(subtensor: SubtensorInterface, verbose: bool): +async def proposals(subtensor: "SubtensorInterface", verbose: bool): console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network From a1ef7630088ac84cdeee94f635d054d8950779b3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 13 Feb 2025 11:07:31 -0800 Subject: [PATCH 332/332] fix fmt error message --- bittensor_cli/src/commands/stake/add.py | 4 ++-- bittensor_cli/src/commands/stake/remove.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 70b02ae8..d400ce96 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -108,7 +108,7 @@ async def safe_stake_extrinsic( return else: err_out( - f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(e)}" ) return else: @@ -181,7 +181,7 @@ async def stake_extrinsic( ) except SubstrateRequestException as e: err_out( - f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(e)}" ) return else: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 1f092ea8..25f53e08 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -667,7 +667,7 @@ async def _safe_unstake_extrinsic( return else: err_out( - f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(e)}" ) return