Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion bittensor_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
from .cli import CLIManager


__version__ = "8.4.3"
__version__ = "8.4.4"

__all__ = ["CLIManager", "__version__"]
6 changes: 4 additions & 2 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions bittensor_cli/src/bittensor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
69 changes: 52 additions & 17 deletions bittensor_cli/src/commands/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
update_metadata_table,
group_subnets,
unlock_key,
blocks_to_duration,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -1242,25 +1257,45 @@ 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,
width=None,
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:
Expand Down
21 changes: 12 additions & 9 deletions tests/e2e_tests/test_senate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading