diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc638055..95ea2f7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 8.4.4 /2025-02-07 - 18:30 PST + +## What's Changed +* Proposals info fix (for dtao governance vote) by @ibraheem-opentensor + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v8.4.3...v8.4.4 + ## 8.4.3 /2025-01-23 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 3c2718ba8..226e8787e 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.4.3" +__version__ = "8.4.4" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 31859bbcc..1d70a22e6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -58,7 +58,7 @@ class GitError(Exception): pass -__version__ = "8.4.3" +__version__ = "8.4.4" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -2798,7 +2798,9 @@ def root_proposals( [green]$[/green] btcli root proposals """ self.verbosity_handler(quiet, verbose) - return self._run_command(root.proposals(self.initialize_chain(network))) + return self._run_command( + root.proposals(self.initialize_chain(network), verbose) + ) def root_set_take( self, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 71d5e12ac..66c7a5c90 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1010,3 +1010,32 @@ def hex_to_bytes(hex_str: str) -> bytes: else: bytes_result = bytes.fromhex(hex_str) return bytes_result + + +def blocks_to_duration(blocks: int) -> str: + """Convert blocks to human readable duration string using two largest units. + + Args: + blocks (int): Number of blocks (12s per block) + + Returns: + str: Duration string like '2d 5h', '3h 45m', '2m 10s', or '0s' + """ + if blocks <= 0: + return "0s" + + seconds = blocks * 12 + intervals = [ + ("d", 86400), # 60 * 60 * 24 + ("h", 3600), # 60 * 60 + ("m", 60), + ("s", 1), + ] + results = [] + for unit, seconds_per_unit in intervals: + unit_count = seconds // seconds_per_unit + seconds %= seconds_per_unit + if unit_count > 0: + results.append(f"{unit_count}{unit}") + # Return only the first two non-zero units + return " ".join(results[:2]) or "0s" diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py index 1dde73df1..668151dbe 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/root.py @@ -42,6 +42,7 @@ update_metadata_table, group_subnets, unlock_key, + blocks_to_duration, ) if TYPE_CHECKING: @@ -82,14 +83,22 @@ def format_call_data(call_data: dict) -> str: 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 arguments, handle nested/large payloads + formatted_args = [] + for arg_name, arg_value in call_args.items(): + if isinstance(arg_value, (tuple, list, dict)): + # For large nested, show abbreviated version + content_str = str(arg_value) + if len(content_str) > 20: + formatted_args.append(f"{arg_name}: ... [{len(content_str)}] ...") + else: + formatted_args.append(f"{arg_name}: {arg_value}") + else: + formatted_args.append(f"{arg_name}: {arg_value}") # Format the final output string - return f"{call_function}({formatted_args})" + args_str = ", ".join(formatted_args) + return f"{module}.{call_function}({args_str})" async def _get_senate_members( @@ -1210,24 +1219,30 @@ async def register(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): ) -async def proposals(subtensor: SubtensorInterface): +async def proposals(subtensor: SubtensorInterface, verbose: bool): 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( + senate_members, all_proposals, current_block = await asyncio.gather( _get_senate_members(subtensor, block_hash), _get_proposals(subtensor, block_hash), + subtensor.substrate.get_block_number(block_hash), ) - print_verbose("Fetching member information from Chain") registered_delegate_info: dict[ str, DelegatesDetails ] = await subtensor.get_delegate_identities() + title = ( + f"[bold #4196D6]Bittensor Governance Proposals[/bold #4196D6]\n" + f"[steel_blue3]Current Block:[/steel_blue3] {current_block}\t" + f"[steel_blue3]Network:[/steel_blue3] {subtensor.network}\n\n" + f"[steel_blue3]Active Proposals:[/steel_blue3] {len(all_proposals)}\t" + f"[steel_blue3]Senate Size:[/steel_blue3] {len(senate_members)}\n" + ) table = Table( Column( "[white]HASH", @@ -1242,8 +1257,8 @@ async def proposals(subtensor: SubtensorInterface): 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}", + Column("[white]CALLDATA", style="dark_sea_green", width=30), + title=title, show_footer=True, box=box.SIMPLE_HEAVY, pad_edge=False, @@ -1251,16 +1266,36 @@ async def proposals(subtensor: SubtensorInterface): border_style="bright_black", ) for hash_, (call_data, vote_data) in all_proposals.items(): + blocks_remaining = vote_data.end - current_block + if blocks_remaining > 0: + duration_str = blocks_to_duration(blocks_remaining) + vote_end_cell = f"{vote_data.end} [dim](in {duration_str})[/dim]" + else: + vote_end_cell = f"{vote_data.end} [red](expired)[/red]" + + ayes_threshold = ( + (len(vote_data.ayes) / vote_data.threshold * 100) + if vote_data.threshold > 0 + else 0 + ) + nays_threshold = ( + (len(vote_data.nays) / vote_data.threshold * 100) + if vote_data.threshold > 0 + else 0 + ) table.add_row( - hash_, + hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}", str(vote_data.threshold), - str(len(vote_data.ayes)), - str(len(vote_data.nays)), + f"{len(vote_data.ayes)} ({ayes_threshold:.2f}%)", + f"{len(vote_data.nays)} ({nays_threshold:.2f}%)", display_votes(vote_data, registered_delegate_info), - str(vote_data.end), + vote_end_cell, format_call_data(call_data), ) - return console.print(table) + console.print(table) + console.print( + "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]" + ) async def set_take(wallet: Wallet, subtensor: SubtensorInterface, take: float) -> bool: diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index f335ada53..5dd9de57b 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -98,9 +98,10 @@ def test_senate(local_chain, wallet_setup): extra_args=[ "--chain", "ws://127.0.0.1:9945", + "--verbose", ], ) - proposals_output = proposals.stdout.splitlines()[8].split() + proposals_output = proposals.stdout.splitlines()[9].split() # Assert the hash is of correct format assert len(proposals_output[0]) == 66 @@ -110,7 +111,7 @@ def test_senate(local_chain, wallet_setup): assert proposals_output[2] == "0" # 0 Nayes for the proposal - assert proposals_output[3] == "0" + assert proposals_output[4] == "0" # Assert initial threshold is 3 assert proposals_output[1] == "3" @@ -143,19 +144,20 @@ def test_senate(local_chain, wallet_setup): extra_args=[ "--chain", "ws://127.0.0.1:9945", + "--verbose", ], ) - proposals_after_aye_output = proposals_after_aye.stdout.splitlines()[8].split() + proposals_after_aye_output = proposals_after_aye.stdout.splitlines()[9].split() # Assert Bob's vote is shown as aye - assert proposals_after_aye_output[4].strip(":") == wallet_bob.hotkey.ss58_address - assert proposals_after_aye_output[5] == "Aye" + assert proposals_after_aye_output[6].strip(":") == wallet_bob.hotkey.ss58_address + assert proposals_after_aye_output[7] == "Aye" # Aye votes increased to 1 assert proposals_after_aye_output[2] == "1" # Nay votes remain 0 - assert proposals_after_aye_output[3] == "0" + assert proposals_after_aye_output[4] == "0" # Register Alice to the root network (0) # Registering to root automatically makes you a senator if eligible @@ -204,18 +206,19 @@ def test_senate(local_chain, wallet_setup): extra_args=[ "--chain", "ws://127.0.0.1:9945", + "--verbose", ], ) proposals_after_nay_output = proposals_after_nay.stdout.splitlines() # Total Ayes to remain 1 - proposals_after_nay_output[8].split()[2] == "1" + proposals_after_nay_output[9].split()[2] == "1" # Total Nays increased to 1 - proposals_after_nay_output[8].split()[3] == "1" + proposals_after_nay_output[9].split()[4] == "1" # Assert Alice has voted Nay - proposals_after_nay_output[9].split()[0].strip( + proposals_after_nay_output[10].split()[0].strip( ":" ) == wallet_alice.hotkey.ss58_address