From 96de08f8e323347ec5645f930c26927e264cbed0 Mon Sep 17 00:00:00 2001 From: Ayden Brewer Date: Wed, 28 Jun 2023 10:31:13 -0700 Subject: [PATCH] BTCli integration with new governance protocol (#1398) * Begin senate cli * Add helper functions to subtensor object for query_module, query_module_map * Remove unused senate helpers * Rename SenateCommand -> ProposalsCommand * Remove unused proposals info datatype * Add proposals data in rich table * Clarify + beautify calldata column * Add SenateRegisterCommand * Add helper function wallet.is_senate_member * Add subtensor *_senate extrinsics, is_senate_member, get_vote_data impl * Use helper function to check senate membership * Add command to view senate members * Use get_vote_data helper and refactor call data formatting for recursion * Add senate_vote and senate_leave cmd classes * Add membership check in senate_register command * Add senate_leave, senate_vote extrinsic functions * Import senate_leave, senate_vote extrinsic functions in subtensor_impl * Add senate, proposal_votes, senate_leave, senate_vote cmds to cli * Move closure helper funcs to main scope, add display_votes helper * Add senate size and active proposals metric, vote overview and nice names * Add delegate nice-name support to proposal_votes * Use coldkey for senate actions instead of hotkey --- bittensor/_cli/__init__.py | 19 + bittensor/_cli/cli_impl.py | 12 + bittensor/_cli/commands/__init__.py | 1 + bittensor/_cli/commands/senate.py | 465 ++++++++++++++++++++++ bittensor/_subtensor/extrinsics/senate.py | 232 +++++++++++ bittensor/_subtensor/subtensor_impl.py | 76 +++- bittensor/_wallet/wallet_impl.py | 15 + 7 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 bittensor/_cli/commands/senate.py create mode 100644 bittensor/_subtensor/extrinsics/senate.py diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 8d727a9696..6f7c58e9ac 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -84,6 +84,13 @@ def __create_parser__() -> 'argparse.ArgumentParser': ListDelegatesCommand.add_args( cmd_parsers ) RegenColdkeypubCommand.add_args( cmd_parsers ) RecycleRegisterCommand.add_args( cmd_parsers ) + SenateCommand.add_args( cmd_parsers ) + ProposalsCommand.add_args( cmd_parsers ) + ShowVotesCommand.add_args( cmd_parsers ) + SenateRegisterCommand.add_args( cmd_parsers ) + SenateLeaveCommand.add_args( cmd_parsers ) + VoteCommand.add_args( cmd_parsers ) + return parser @@ -156,6 +163,18 @@ def check_config (config: 'bittensor.Config'): MyDelegatesCommand.check_config( config ) elif config.command == "recycle_register": RecycleRegisterCommand.check_config( config ) + elif config.command == "senate": + SenateCommand.check_config( config ) + elif config.command == "proposals": + ProposalsCommand.check_config( config ) + elif config.command == "proposal_votes": + ShowVotesCommand.check_config( config ) + elif config.command == "senate_register": + SenateRegisterCommand.check_config( config ) + elif config.command == "senate_leave": + SenateLeaveCommand.check_config( config ) + elif config.command == "senate_vote": + VoteCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 6c0f6dde80..5e1cfb3d84 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -82,4 +82,16 @@ def run ( self ): ListSubnetsCommand.run( self ) elif self.config.command == 'recycle_register': RecycleRegisterCommand.run( self ) + elif self.config.command == "senate": + SenateCommand.run( self ) + elif self.config.command == "proposals": + ProposalsCommand.run( self ) + elif self.config.command == "proposal_votes": + ShowVotesCommand.run( self ) + elif self.config.command == "senate_register": + SenateRegisterCommand.run( self ) + elif self.config.command == "senate_leave": + SenateLeaveCommand.run( self ) + elif self.config.command == "senate_vote": + VoteCommand.run( self ) diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index 5d501da284..0a53ed3cc8 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -9,3 +9,4 @@ from .metagraph import MetagraphCommand from .list import ListCommand from .misc import UpdateCommand, ListSubnetsCommand +from .senate import SenateCommand, ProposalsCommand, ShowVotesCommand, SenateRegisterCommand, SenateLeaveCommand, VoteCommand diff --git a/bittensor/_cli/commands/senate.py b/bittensor/_cli/commands/senate.py new file mode 100644 index 0000000000..b2e84f991b --- /dev/null +++ b/bittensor/_cli/commands/senate.py @@ -0,0 +1,465 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import sys +import argparse +import bittensor +from rich.prompt import Prompt, Confirm +from rich.table import Table +from typing import List, Union, Optional, Dict, Tuple +console = bittensor.__console__ + +class SenateCommand: + + @staticmethod + def run( cli ): + r""" View Bittensor's governance protocol proposals + """ + config = cli.config.copy() + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + senate_members = subtensor.query_module("Senate", "Members").serialize() + + table = Table(show_footer=False) + table.title = ( + "[white]Senate" + ) + table.add_column("[overline white]ADDRESS", footer_style = "overline white", style='yellow', no_wrap=True) + table.show_footer = True + + for ss58_address in senate_members: + table.add_row( + ss58_address + ) + + table.box = None + table.pad_edge = False + table.width = None + console.print(table) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + None + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_parser = parser.add_parser( + 'senate', + help='''View senate and it's members''' + ) + senate_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_parser ) + bittensor.subtensor.add_args( senate_parser ) + +from .utils import get_delegates_details, DelegatesDetails +def format_call_data(call_data: List) -> str: + human_call_data = list() + + for arg in call_data["call_args"]: + arg_value = arg["value"] + + # If this argument is a nested call + func_args = format_call_data({ + "call_function": arg_value["call_function"], + "call_args": arg_value["call_args"] + }) if isinstance(arg_value, dict) and "call_function" in arg_value else str(arg_value) + + human_call_data.append("{}: {}".format(arg["name"], func_args)) + + return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) + +def display_votes(vote_data, delegate_info) -> str: + vote_list = list() + + for address in vote_data["ayes"]: + vote_list.append("{}: {}".format(delegate_info[address].name 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].name if address in delegate_info else address, "[bold red]Nay[/bold red]")) + + return "\n".join(vote_list) + +class ProposalsCommand: + + @staticmethod + def run( cli ): + r""" View Bittensor's governance protocol proposals + """ + config = cli.config.copy() + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + senate_members = subtensor.query_module("SenateMembers", "Members").serialize() + proposals = dict() + proposal_hashes = subtensor.query_module("Triumvirate", "Proposals") + + for hash in proposal_hashes: + proposals[hash] = [ + subtensor.query_module("Triumvirate", "ProposalOf", None, [hash]), + subtensor.get_vote_data( hash ) + ] + + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) + + table = Table(show_footer=False) + table.title = ( + "[white]Proposals\t\tActive Proposals: {}\t\tSenate Size: {}".format(len(proposals), len(senate_members)) + ) + table.add_column("[overline white]HASH", footer_style = "overline white", style='yellow', no_wrap=True) + table.add_column("[overline white]THRESHOLD", footer_style = "overline white", style='white') + table.add_column("[overline white]AYES", footer_style = "overline white", style='green') + table.add_column("[overline white]NAYS", footer_style = "overline white", style='red') + table.add_column("[overline white]VOTES", footer_style = "overline white", style='rgb(50,163,219)') + table.add_column("[overline white]END", footer_style = "overline white", style='blue') + table.add_column("[overline white]CALLDATA", footer_style = "overline white", style='white') + table.show_footer = True + + for hash in proposals: + call_data = proposals[hash][0].serialize() + vote_data = proposals[hash][1] + + 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) + ) + + table.box = None + table.pad_edge = False + table.width = None + console.print(table) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + None + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + proposals_parser = parser.add_parser( + 'proposals', + help='''View active triumvirate proposals and their status''' + ) + proposals_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + proposals_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( proposals_parser ) + bittensor.subtensor.add_args( proposals_parser ) + +class ShowVotesCommand: + + @staticmethod + def run( cli ): + r""" View Bittensor's governance protocol proposals active votes + """ + config = cli.config.copy() + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + console.print(":satellite: Syncing with chain: [white]{}[/white] ...".format(cli.config.subtensor.network)) + + proposal_hash = cli.config.proposal_hash + if len(proposal_hash) == 0: + console.print('Aborting: Proposal hash not specified. View all proposals with the "proposals" command.') + return + + proposal_vote_data = subtensor.get_vote_data( proposal_hash ) + if proposal_vote_data == None: + console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return + + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(url = bittensor.__delegates_details_url__) + + table = Table(show_footer=False) + table.title = ( + "[white]Votes for Proposal {}".format(proposal_hash) + ) + table.add_column("[overline white]ADDRESS", footer_style = "overline white", style='yellow', no_wrap=True) + table.add_column("[overline white]VOTE", footer_style = "overline white", style='white') + table.show_footer = True + + votes = display_votes(proposal_vote_data, registered_delegate_info).split("\n") + for vote in votes: + split_vote_data = vote.split(": ") # Nasty, but will work. + table.add_row( + split_vote_data[0], + split_vote_data[1] + ) + + table.box = None + table.pad_edge = False + table.min_width = 64 + console.print(table) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if config.proposal_hash == "" and not config.no_prompt: + proposal_hash = Prompt.ask("Enter proposal hash") + config.proposal_hash = str(proposal_hash) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + show_votes_parser = parser.add_parser( + 'proposal_votes', + help='''View an active proposal's votes by address.''' + ) + show_votes_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + show_votes_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + show_votes_parser.add_argument( + '--proposal', + dest='proposal_hash', + type=str, + nargs='?', + help='''Set the proposal to show votes for.''', + default="" + ) + bittensor.wallet.add_args( show_votes_parser ) + bittensor.subtensor.add_args( show_votes_parser ) + +class SenateRegisterCommand: + + @staticmethod + def run( cli ): + r""" Register to participate in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + # Check if the hotkey is a delegate. + if not subtensor.is_hotkey_delegate( wallet.hotkey.ss58_address ): + console.print('Aborting: Hotkey {} isn\'t a delegate.'.format(wallet.hotkey.ss58_address)) + return + + if wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} is already a senate member.'.format(wallet.hotkey.ss58_address)) + return + + subtensor.register_senate( + wallet = wallet, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_register_parser = parser.add_parser( + 'senate_register', + help='''Register as a senate member to participate in proposals''' + ) + senate_register_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_register_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_register_parser ) + bittensor.subtensor.add_args( senate_register_parser ) + +class SenateLeaveCommand: + + @staticmethod + def run( cli ): + r""" Discard membership in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + if not wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) + return + + subtensor.leave_senate( + wallet = wallet, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + senate_leave_parser = parser.add_parser( + 'senate_leave', + help='''Discard senate membership in the governance protocol''' + ) + senate_leave_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + senate_leave_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( senate_leave_parser ) + bittensor.subtensor.add_args( senate_leave_parser ) + +class VoteCommand: + + @staticmethod + def run( cli ): + r""" Vote in Bittensor's governance protocol proposals + """ + config = cli.config.copy() + wallet = bittensor.wallet( config = cli.config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + + proposal_hash = cli.config.proposal_hash + if len(proposal_hash) == 0: + console.print('Aborting: Proposal hash not specified. View all proposals with the "proposals" command.') + return + + if not wallet.is_senate_member(subtensor): + console.print('Aborting: Hotkey {} isn\'t a senate member.'.format(wallet.hotkey.ss58_address)) + return + + # Unlock the wallet. + wallet.hotkey + wallet.coldkey + + vote_data = subtensor.get_vote_data( proposal_hash ) + if vote_data == None: + console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return + + vote = Confirm.ask("Desired vote for proposal") + subtensor.vote_senate( + wallet = wallet, + proposal_hash = proposal_hash, + proposal_idx = vote_data["index"], + vote = vote, + prompt = not cli.config.no_prompt + ) + + @classmethod + def check_config( cls, config: 'bittensor.Config' ): + if not config.is_set('wallet.name') and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + if not config.is_set('wallet.hotkey') and not config.no_prompt: + hotkey = Prompt.ask("Enter hotkey name", default = bittensor.defaults.wallet.hotkey) + config.wallet.hotkey = str(hotkey) + + if config.proposal_hash == "" and not config.no_prompt: + proposal_hash = Prompt.ask("Enter proposal hash") + config.proposal_hash = str(proposal_hash) + + @classmethod + def add_args( cls, parser: argparse.ArgumentParser ): + vote_parser = parser.add_parser( + 'senate_vote', + help='''Vote on an active proposal by hash.''' + ) + vote_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + vote_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + vote_parser.add_argument( + '--proposal', + dest='proposal_hash', + type=str, + nargs='?', + help='''Set the proposal to show votes for.''', + default="" + ) + bittensor.wallet.add_args( vote_parser ) + bittensor.subtensor.add_args( vote_parser ) diff --git a/bittensor/_subtensor/extrinsics/senate.py b/bittensor/_subtensor/extrinsics/senate.py new file mode 100644 index 0000000000..66e29cb14c --- /dev/null +++ b/bittensor/_subtensor/extrinsics/senate.py @@ -0,0 +1,232 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Imports +import bittensor + +import time +from rich.prompt import Confirm +from ..errors import * + +def register_senate_extrinsic ( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False +) -> bool: + r""" Registers the wallet to chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + 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. + wait_for_finalization (bool): + 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. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( f"Register delegate hotkey to senate?" ): + return False + + with bittensor.__console__.status(":satellite: Registering with senate..."): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='join_senate', + call_params={ + "hotkey": wallet.hotkey.ss58_address + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if registration successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful registration, final check for membership + else: + is_registered = wallet.is_senate_member(subtensor) + + if is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Registered[/green]") + return True + else: + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Senate membership not found.[/red]") + +def leave_senate_extrinsic ( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.Wallet', + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False +) -> bool: + r""" Removes the wallet from chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + 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. + wait_for_finalization (bool): + 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. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( f"Remove delegate hotkey from senate?" ): + return False + + with bittensor.__console__.status(":satellite: Leaving senate..."): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='leave_senate', + call_params={ + "hotkey": wallet.hotkey.ss58_address + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if registration successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful registration, final check for membership + else: + is_registered = wallet.is_senate_member(subtensor) + + if not is_registered: + bittensor.__console__.print(":white_heavy_check_mark: [green]Left senate[/green]") + return True + else: + # neuron not found, try again + bittensor.__console__.print(":cross_mark: [red]Unknown error. Senate membership still found.[/red]") + +def vote_senate_extrinsic ( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.Wallet', + proposal_hash: str, + proposal_idx: int, + vote: bool, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False +) -> bool: + r""" Removes the wallet from chain for senate voting. + Args: + wallet (bittensor.wallet): + bittensor wallet object. + wait_for_inclusion (bool): + 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. + wait_for_finalization (bool): + 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. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + If we did not wait for finalization / inclusion, the response is true. + """ + wallet.coldkey # unlock coldkey + wallet.hotkey # unlock hotkey + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask( "Cast a vote of {}?".format( vote ) ): + return False + + with bittensor.__console__.status( ":satellite: Casting vote.." ): + with subtensor.substrate as substrate: + # create extrinsic call + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='vote', + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "proposal": proposal_hash, + "index": proposal_idx, + "approve": vote + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization ) + + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + + # process if vote successful + response.process_events() + if not response.is_success: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + time.sleep(0.5) + + # Successful vote, final check for data + else: + vote_data = subtensor.get_vote_data( proposal_hash ) + has_voted = vote_data["ayes"].count( wallet.hotkey.ss58_address ) > 0 or vote_data["nays"].count( wallet.hotkey.ss58_address ) > 0 + + if has_voted: + bittensor.__console__.print(":white_heavy_check_mark: [green]Vote cast.[/green]") + return True + else: + # hotkey not found in ayes/nays + bittensor.__console__.print(":cross_mark: [red]Unknown error. Couldn't find vote.[/red]") \ No newline at end of file diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 8ec3477b5e..e21d6f8058 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -39,6 +39,7 @@ from .extrinsics.set_weights import set_weights_extrinsic from .extrinsics.prometheus import prometheus_extrinsic from .extrinsics.delegation import delegate_extrinsic, nominate_extrinsic,undelegate_extrinsic +from .extrinsics.senate import register_senate_extrinsic, leave_senate_extrinsic, vote_senate_extrinsic from .types import AxonServeCallParams, PrometheusServeCallParams # Logging @@ -643,8 +644,6 @@ def unstake_multiple ( """ Removes stake from each hotkey_ss58 in the list, using each amount, to a common coldkey. """ return unstake_multiple_extrinsic( self, wallet, hotkey_ss58s, amounts, wait_for_inclusion, wait_for_finalization, prompt) - - def unstake ( self, wallet: 'bittensor.wallet', @@ -698,6 +697,53 @@ def _do_unstake( else: raise StakeError(response.error_message) + ################ + #### Senate #### + ################ + + def register_senate( + self, + wallet: 'bittensor.wallet', + wait_for_inclusion:bool = True, + wait_for_finalization:bool = False, + prompt: bool = False, + ) -> bool: + return register_senate_extrinsic( self, wallet, wait_for_inclusion, wait_for_finalization, prompt ) + + def leave_senate( + self, + wallet: 'bittensor.wallet', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + return leave_senate_extrinsic( self, wallet, wait_for_inclusion, wait_for_finalization, prompt ) + + def vote_senate( + self, + wallet: 'bittensor.wallet', + proposal_hash: str, + proposal_idx: int, + vote: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + return vote_senate_extrinsic( self, wallet, proposal_hash, proposal_idx, vote, wait_for_inclusion, wait_for_finalization, prompt ) + + def is_senate_member( + self, + hotkey_ss58: str + ) -> bool: + senate_members = self.query_module("Senate", "Members").serialize() + return senate_members.count( hotkey_ss58 ) > 0 + + def get_vote_data( + self, + proposal_hash: str + ) -> Optional[dict]: + vote_data = self.query_module("Triumvirate", "Voting", None, [proposal_hash]) + return vote_data.serialize() if vote_data != None else None ######################## #### Standard Calls #### @@ -740,6 +786,32 @@ def make_substrate_call_with_retry(): block_hash = None if block == None else substrate.get_block_hash(block) ) return make_substrate_call_with_retry() + + """ Queries any module storage with params and block. """ + def query_module( self, module: str, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[object]: + @retry(delay=2, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + with self.substrate as substrate: + return substrate.query( + module=module, + storage_function = name, + params = params, + block_hash = None if block == None else substrate.get_block_hash(block) + ) + return make_substrate_call_with_retry() + + """ Queries any module map storage with params and block. """ + def query_map( self, module: str, name: str, block: Optional[int] = None, params: Optional[List[object]] = [] ) -> Optional[object]: + @retry(delay=2, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + with self.substrate as substrate: + return substrate.query_map( + module=module, + storage_function = name, + params = params, + block_hash = None if block == None else substrate.get_block_hash(block) + ) + return make_substrate_call_with_retry() ##################################### #### Hyper parameter calls. #### diff --git a/bittensor/_wallet/wallet_impl.py b/bittensor/_wallet/wallet_impl.py index dd82d676fa..af91191065 100644 --- a/bittensor/_wallet/wallet_impl.py +++ b/bittensor/_wallet/wallet_impl.py @@ -175,6 +175,21 @@ def is_registered( self, subtensor: Optional['bittensor.Subtensor'] = None, netu return subtensor.is_hotkey_registered_any( self.hotkey.ss58_address ) else: return subtensor.is_hotkey_registered_on_subnet( self.hotkey.ss58_address, netuid = netuid ) + + def is_senate_member( self, subtensor: Optional['bittensor.Subtensor'] = None ) -> bool: + """ Returns true if this wallet is registered as a senate member. + Args: + subtensor( Optional['bittensor.Subtensor'] ): + Bittensor subtensor connection. Overrides with defaults if None. + Determines which network we check for senate membership. + Return: + is_registered (bool): + Is the wallet apart of the senate. + """ + if subtensor == None: subtensor = bittensor.subtensor(self.config) + + # default to finney + return subtensor.is_senate_member( self.hotkey.ss58_address ) def get_neuron ( self, netuid: int, subtensor: Optional['bittensor.Subtensor'] = None ) -> Optional['bittensor.NeuronInfo'] :