diff --git a/bittensor_cli/src/bittensor/json_utils.py b/bittensor_cli/src/bittensor/json_utils.py new file mode 100644 index 000000000..f789e33df --- /dev/null +++ b/bittensor_cli/src/bittensor/json_utils.py @@ -0,0 +1,277 @@ +""" +Standardized JSON output utilities for btcli. + +This module provides consistent JSON response formatting across all btcli commands. +All JSON outputs should use these utilities to ensure schema compliance. + +Standard Transaction Response Format: +{ + "success": bool, # Required: Whether the operation succeeded + "message": str | None, # Optional: Human-readable message + "extrinsic_identifier": str | None # Optional: Block-extrinsic ID (e.g., "12345-2") +} + +Standard Data Response Format: +{ + "success": bool, # Required: Whether the operation succeeded + "data": {...}, # Optional: Command-specific response data + "error": str # Optional: Error message if success=False +} +""" + +import json +from typing import Any, Optional, Union +from rich.console import Console + +json_console = Console() + + +def transaction_response( + success: bool, + message: Optional[str] = None, + extrinsic_identifier: Optional[str] = None, +) -> dict[str, Any]: + """ + Create a standardized transaction response dictionary. + + Args: + success: Whether the transaction succeeded + message: Human-readable status message + extrinsic_identifier: The extrinsic ID (e.g., "12345678-2") + + Returns: + Dictionary with standardized transaction format + """ + return { + "success": success, + "message": message, + "extrinsic_identifier": extrinsic_identifier, + } + + +def print_transaction_response( + success: bool, + message: Optional[str] = None, + extrinsic_identifier: Optional[str] = None, +) -> None: + """ + Print a standardized transaction response as JSON. + + Args: + success: Whether the transaction succeeded + message: Human-readable status message + extrinsic_identifier: The extrinsic ID (e.g., "12345678-2") + """ + json_console.print_json(data=transaction_response(success, message, extrinsic_identifier)) + + +class TransactionResult: + """ + Helper class for building transaction responses. + + Provides a clean interface for transaction commands that need to + build up response data before printing. + """ + + def __init__( + self, + success: bool, + message: Optional[str] = None, + extrinsic_identifier: Optional[str] = None, + ): + self.success = success + self.message = message + self.extrinsic_identifier = extrinsic_identifier + + def as_dict(self) -> dict[str, Any]: + """Return the response as a dictionary.""" + return transaction_response( + self.success, + self.message, + self.extrinsic_identifier, + ) + + def print(self) -> None: + """Print the response as JSON.""" + json_console.print_json(data=self.as_dict()) + + +class MultiTransactionResult: + """ + Helper class for commands that process multiple transactions. + + Builds a keyed dictionary of transaction results. + """ + + def __init__(self): + self._results: dict[str, TransactionResult] = {} + + def add( + self, + key: str, + success: bool, + message: Optional[str] = None, + extrinsic_identifier: Optional[str] = None, + ) -> None: + """Add a transaction result with the given key.""" + self._results[key] = TransactionResult(success, message, extrinsic_identifier) + + def add_result(self, key: str, result: TransactionResult) -> None: + """Add an existing TransactionResult with the given key.""" + self._results[key] = result + + def as_dict(self) -> dict[str, dict[str, Any]]: + """Return all results as a dictionary.""" + return {k: v.as_dict() for k, v in self._results.items()} + + def print(self) -> None: + """Print all results as JSON.""" + json_console.print_json(data=self.as_dict()) + + +def json_response( + success: bool, + data: Optional[Any] = None, + error: Optional[str] = None, +) -> str: + """ + Create a standardized JSON response string for data queries. + + Args: + success: Whether the operation succeeded + data: Optional response data (dict, list, or primitive) + error: Optional error message (typically used when success=False) + + Returns: + JSON string with standardized format + + Examples: + >>> json_response(True, {"balance": 100.5}) + '{"success": true, "data": {"balance": 100.5}}' + + >>> json_response(False, error="Wallet not found") + '{"success": false, "error": "Wallet not found"}' + """ + response: dict[str, Any] = {"success": success} + + if data is not None: + response["data"] = data + + if error is not None: + response["error"] = error + + return json.dumps(response) + + +def json_success(data: Any) -> str: + """ + Create a successful JSON response string. + + Args: + data: Response data to include + + Returns: + JSON string with success=True and the provided data + """ + return json_response(success=True, data=data) + + +def json_error(error: str, data: Optional[Any] = None) -> str: + """ + Create an error JSON response string. + + Args: + error: Error message describing what went wrong + data: Optional additional context data + + Returns: + JSON string with success=False and error message + """ + return json_response(success=False, data=data, error=error) + + +def print_json(response: str) -> None: + """ + Print a JSON string response to the console. + + Args: + response: JSON string to print + """ + json_console.print(response) + + +def print_json_success(data: Any) -> None: + """ + Print a successful JSON response. + + Args: + data: Response data to include + """ + print_json(json_success(data)) + + +def print_json_error(error: str, data: Optional[Any] = None) -> None: + """ + Print an error JSON response. + + Args: + error: Error message + data: Optional additional context + """ + print_json(json_error(error, data)) + + +def print_json_data(data: Any) -> None: + """ + Print data directly as JSON (for simple data responses). + + Args: + data: Data to print as JSON + """ + json_console.print_json(data=data) + + +def print_transaction_with_data( + success: bool, + message: Optional[str] = None, + extrinsic_identifier: Optional[str] = None, + **extra_data: Any, +) -> None: + """ + Print a transaction response with additional data fields. + + Args: + success: Whether the transaction succeeded + message: Human-readable status message + extrinsic_identifier: The extrinsic ID (e.g., "12345678-2") + **extra_data: Additional fields to include in the response + """ + response = { + "success": success, + "message": message, + "extrinsic_identifier": extrinsic_identifier, + **extra_data, + } + json_console.print_json(data=response) + + +def serialize_balance(balance: Any) -> dict[str, Union[int, float]]: + """ + Serialize a Balance object to a consistent dictionary format. + + Args: + balance: A Balance object or numeric value + + Returns: + Dictionary with 'rao' (int) and 'tao' (float) keys + """ + if hasattr(balance, "rao") and hasattr(balance, "tao"): + return {"rao": int(balance.rao), "tao": float(balance.tao)} + elif isinstance(balance, (int, float)): + # Assume it's already in tao if float, rao if int + if isinstance(balance, float): + return {"rao": int(balance * 1e9), "tao": balance} + else: + return {"rao": balance, "tao": balance / 1e9} + else: + return {"rao": 0, "tao": 0.0} diff --git a/bittensor_cli/src/commands/axon/axon.py b/bittensor_cli/src/commands/axon/axon.py index ebd7404cd..a904ff096 100644 --- a/bittensor_cli/src/commands/axon/axon.py +++ b/bittensor_cli/src/commands/axon/axon.py @@ -2,15 +2,12 @@ Axon commands for managing neuron serving endpoints. """ -import json from typing import TYPE_CHECKING from bittensor_wallet import Wallet -from bittensor_cli.src.bittensor.utils import ( - print_error, - json_console, -) +from bittensor_cli.src.bittensor.utils import print_error +from bittensor_cli.src.bittensor.json_utils import print_transaction_with_data from bittensor_cli.src.bittensor.extrinsics.serving import ( reset_axon_extrinsic, set_axon_extrinsic, @@ -54,16 +51,12 @@ async def reset( ) if json_output: - json_console.print( - json.dumps( - { - "success": success, - "message": message, - "extrinsic_identifier": ext_id, - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - } - ) + print_transaction_with_data( + success=success, + message=message, + extrinsic_identifier=ext_id, + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, ) elif not success: print_error(f"Failed to reset axon: {message}") @@ -115,18 +108,14 @@ async def set_axon( ) if json_output: - json_console.print( - json.dumps( - { - "success": success, - "message": message, - "extrinsic_identifier": ext_id, - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "ip": ip, - "port": port, - } - ) + print_transaction_with_data( + success=success, + message=message, + extrinsic_identifier=ext_id, + netuid=netuid, + hotkey=wallet.hotkey.ss58_address, + ip=ip, + port=port, ) elif not success: print_error(f"Failed to set axon: {message}") diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 0e0097a95..b88c7056a 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -1,4 +1,3 @@ -import json from typing import Optional from async_substrate_interface.utils.cache import asyncio @@ -13,12 +12,12 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, - json_console, print_error, print_extrinsic_id, print_success, unlock_key, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data from bittensor_cli.src.commands.crowd.view import show_crowdloan_details from bittensor_cli.src.bittensor.chain_data import CrowdloanData @@ -88,7 +87,7 @@ async def contribute_to_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} not found." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, error_msg @@ -98,7 +97,7 @@ async def contribute_to_crowdloan( ) if not is_valid: if json_output: - json_console.print(json.dumps({"success": False, "error": error_message})) + print_json_data({"success": False, "error": error_message}) else: print_error(error_message) return False, error_message @@ -135,7 +134,7 @@ async def contribute_to_crowdloan( if contribution_amount < crowdloan.min_contribution: error_msg = f"Contribution amount ({contribution_amount}) is below minimum ({crowdloan.min_contribution})." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Contribution below minimum requirement." @@ -143,7 +142,7 @@ async def contribute_to_crowdloan( if contribution_amount > user_balance: error_msg = f"Insufficient balance. You have {user_balance} but trying to contribute {contribution_amount}." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Insufficient balance." @@ -230,7 +229,7 @@ async def contribute_to_crowdloan( "\nProceed with contribution?", decline=decline, quiet=quiet ): if json_output: - json_console.print( + print_json_data( json.dumps( {"success": False, "error": "Contribution cancelled by user."} ) @@ -242,7 +241,7 @@ async def contribute_to_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -264,7 +263,7 @@ async def contribute_to_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -319,7 +318,7 @@ async def contribute_to_crowdloan( else False, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: console.print( f"\n[dark_sea_green3]Successfully contributed to crowdloan #{crowdloan_id}![/dark_sea_green3]" @@ -397,7 +396,7 @@ async def withdraw_from_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} does not exist." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, error_msg @@ -405,7 +404,7 @@ async def withdraw_from_crowdloan( if crowdloan.finalized: error_msg = f"Crowdloan #{crowdloan_id} is already finalized. Withdrawals are not allowed." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Cannot withdraw from finalized crowdloan." @@ -423,7 +422,7 @@ async def withdraw_from_crowdloan( f"You have no contribution to withdraw from crowdloan #{crowdloan_id}." ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "No contribution to withdraw." @@ -434,7 +433,7 @@ async def withdraw_from_crowdloan( if withdrawable <= 0: error_msg = f"As the creator, you cannot withdraw your deposit of {crowdloan.deposit}. Only contributions above the deposit can be withdrawn." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Creator cannot withdraw deposit amount." @@ -519,7 +518,7 @@ async def withdraw_from_crowdloan( "\nProceed with withdrawal?", decline=decline, quiet=quiet ): if json_output: - json_console.print( + print_json_data( json.dumps( {"success": False, "error": "Withdrawal cancelled by user."} ) @@ -531,7 +530,7 @@ async def withdraw_from_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -553,7 +552,7 @@ async def withdraw_from_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -599,7 +598,7 @@ async def withdraw_from_crowdloan( }, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: print_success(f"Successfully withdrew from crowdloan #{crowdloan_id}!\n") diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index f14d4aae3..40e145adf 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import Optional from bittensor_wallet import Wallet @@ -16,13 +15,13 @@ blocks_to_duration, confirm_action, console, - json_console, print_error, print_success, is_valid_ss58_address, unlock_key, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data async def create_crowdloan( @@ -52,9 +51,7 @@ async def create_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( - json.dumps({"success": False, "error": unlock_status.message}) - ) + print_json_data({"success": False, "error": unlock_status.message}) else: print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message @@ -89,7 +86,7 @@ async def create_crowdloan( else: error_msg = "Crowdloan type not specified and no prompt provided." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, error_msg @@ -127,7 +124,7 @@ async def create_crowdloan( + ", ".join(missing_fields) ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, "Missing required options when prompts are disabled." @@ -149,7 +146,7 @@ async def create_crowdloan( continue error_msg = f"Deposit is below the minimum required deposit ({minimum_deposit.tao} TAO)." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, "Deposit is below the minimum required deposit." @@ -352,7 +349,7 @@ async def create_crowdloan( "Proceed with creating the crowdloan?", decline=decline, quiet=quiet ): if json_output: - json_console.print( + print_json_data( json.dumps( {"success": False, "error": "Cancelled crowdloan creation."} ) @@ -371,7 +368,7 @@ async def create_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -406,7 +403,7 @@ async def create_crowdloan( else: output_dict["data"]["target_address"] = target_address - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) message = f"{crowdloan_type.capitalize()} crowdloan created successfully." else: if crowdloan_type == "subnet": @@ -484,7 +481,7 @@ async def finalize_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} does not exist." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, error_msg @@ -494,7 +491,7 @@ async def finalize_crowdloan( f"Only the creator can finalize a crowdloan. Creator: {crowdloan.creator}" ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Only the creator can finalize a crowdloan." @@ -502,7 +499,7 @@ async def finalize_crowdloan( if crowdloan.finalized: error_msg = f"Crowdloan #{crowdloan_id} is already finalized." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(error_msg) return False, "Crowdloan is already finalized." @@ -514,7 +511,7 @@ async def finalize_crowdloan( f"Cap: {crowdloan.cap.tao}, Still needed: {still_needed.tao}" ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"Crowdloan #{crowdloan_id} has not reached its cap.\n" @@ -603,7 +600,7 @@ async def finalize_crowdloan( "\nProceed with finalization?", decline=decline, quiet=quiet ): if json_output: - json_console.print( + print_json_data( json.dumps( {"success": False, "error": "Finalization cancelled by user."} ) @@ -615,7 +612,7 @@ async def finalize_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -632,7 +629,7 @@ async def finalize_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -661,7 +658,7 @@ async def finalize_crowdloan( "call_executed": crowdloan.has_call, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: console.print( f"\n[dark_sea_green3]Successfully finalized crowdloan #{crowdloan_id}![/dark_sea_green3]\n" diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index de134d964..ce13027cf 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import Optional from bittensor_wallet import Wallet @@ -12,11 +11,11 @@ blocks_to_duration, confirm_action, console, - json_console, print_extrinsic_id, print_error, unlock_key, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data async def dissolve_crowdloan( @@ -60,7 +59,7 @@ async def dissolve_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} not found." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, error_msg @@ -70,7 +69,7 @@ async def dissolve_crowdloan( f"Crowdloan #{crowdloan_id} is already finalized and cannot be dissolved." ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is finalized." @@ -78,7 +77,7 @@ async def dissolve_crowdloan( if creator_ss58 != crowdloan.creator: error_msg = f"Only the creator can dissolve this crowdloan. Creator: {crowdloan.creator}, Your address: {creator_ss58}" if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Only the creator can dissolve this crowdloan.[/red]\n" @@ -98,7 +97,7 @@ async def dissolve_crowdloan( "Run 'btcli crowd refund' until only the creator's funds remain." ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Crowdloan still holds funds from other contributors.[/red]\n" @@ -146,7 +145,7 @@ async def dissolve_crowdloan( quiet=quiet, ): if json_output: - json_console.print( + print_json_data( json.dumps( {"success": False, "error": "Dissolution cancelled by user."} ) @@ -158,7 +157,7 @@ async def dissolve_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -187,7 +186,7 @@ async def dissolve_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -211,7 +210,7 @@ async def dissolve_crowdloan( "total_dissolved": creator_contribution.tao, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: await print_extrinsic_id(extrinsic_receipt) console.print("[green]Crowdloan dissolved successfully![/green]") diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index 6fbff652c..de81d554c 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import Optional from bittensor_wallet import Wallet @@ -10,11 +9,11 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, - json_console, print_extrinsic_id, print_error, unlock_key, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data from bittensor_cli.src.commands.crowd.view import show_crowdloan_details from bittensor_cli.src.commands.crowd.utils import get_constant @@ -62,7 +61,7 @@ async def refund_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} not found." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, error_msg @@ -70,7 +69,7 @@ async def refund_crowdloan( if crowdloan.finalized: error_msg = f"Crowdloan #{crowdloan_id} is already finalized. Finalized crowdloans cannot be refunded." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." @@ -78,7 +77,7 @@ async def refund_crowdloan( if creator_ss58 != crowdloan.creator: error_msg = f"Only the creator can refund this crowdloan. Creator: {crowdloan.creator}, Your address: {creator_ss58}" if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Only the creator can refund this crowdloan.[/red]\n" @@ -156,7 +155,7 @@ async def refund_crowdloan( quiet=quiet, ): if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": "Refund cancelled by user."}) ) else: @@ -166,7 +165,7 @@ async def refund_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -197,7 +196,7 @@ async def refund_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -223,7 +222,7 @@ async def refund_crowdloan( "amount_refunded": (crowdloan.raised - crowdloan.deposit).tao, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: console.print( f"[green]Contributors have been refunded for Crowdloan #{crowdloan_id}.[/green]" diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 6abefed55..47d642d15 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import Optional, Union from bittensor_wallet import Wallet @@ -13,11 +12,11 @@ blocks_to_duration, confirm_action, console, - json_console, print_error, unlock_key, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data from bittensor_cli.src.commands.crowd.view import show_crowdloan_details from bittensor_cli.src.commands.crowd.utils import get_constant @@ -73,7 +72,7 @@ async def update_crowdloan( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} not found." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, error_msg @@ -83,7 +82,7 @@ async def update_crowdloan( f"Crowdloan #{crowdloan_id} is already finalized and cannot be updated." ) if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." @@ -92,7 +91,7 @@ async def update_crowdloan( if creator_address != crowdloan.creator: error_msg = "Only the creator can update this crowdloan." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Only the creator can update this crowdloan.[/red]\n" @@ -127,7 +126,7 @@ async def update_crowdloan( if choice == 4: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": "Update cancelled by user."}) ) else: @@ -243,7 +242,7 @@ async def update_crowdloan( if call_function is None or value is None or param_name is None: error_msg = "No update parameter specified." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, error_msg @@ -253,7 +252,7 @@ async def update_crowdloan( if value.rao < absolute_min.rao: error_msg = f"Minimum contribution must be at least {absolute_min}." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Minimum contribution ({value}) must be at least {absolute_min}.[/red]" @@ -264,7 +263,7 @@ async def update_crowdloan( if value <= current_block: error_msg = "End block must be in the future." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]End block ({value:,}) must be after current block ({current_block:,}).[/red]" @@ -275,7 +274,7 @@ async def update_crowdloan( if block_duration < min_duration: error_msg = "Block duration too short." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Duration ({blocks_to_duration(block_duration)}) is too short. " @@ -286,7 +285,7 @@ async def update_crowdloan( if block_duration > max_duration: error_msg = "Block duration too long." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]Duration ({blocks_to_duration(block_duration)}) is too long. " @@ -298,7 +297,7 @@ async def update_crowdloan( if value < crowdloan.raised: error_msg = "Cap must be >= raised amount." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error( f"[red]New cap ({value}) must be at least the amount already raised ({crowdloan.raised}).[/red]" @@ -341,7 +340,7 @@ async def update_crowdloan( quiet=quiet, ): if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": "Update cancelled by user."}) ) else: @@ -351,7 +350,7 @@ async def update_crowdloan( unlock_status = unlock_key(wallet) if not unlock_status.success: if json_output: - json_console.print( + print_json_data( json.dumps({"success": False, "error": unlock_status.message}) ) else: @@ -384,7 +383,7 @@ async def update_crowdloan( if not success: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -407,7 +406,7 @@ async def update_crowdloan( "update_type": update_type, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) else: console.print( f"[green]{update_type} updated successfully![/green]\n" diff --git a/bittensor_cli/src/commands/crowd/view.py b/bittensor_cli/src/commands/crowd/view.py index 9a248d18f..1262570e4 100644 --- a/bittensor_cli/src/commands/crowd/view.py +++ b/bittensor_cli/src/commands/crowd/view.py @@ -1,7 +1,6 @@ from typing import Optional import asyncio -import json from bittensor_wallet import Wallet from rich import box from rich.table import Column, Table @@ -13,10 +12,10 @@ from bittensor_cli.src.bittensor.utils import ( blocks_to_duration, console, - json_console, print_error, millify_tao, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data def _shorten(account: Optional[str]) -> str: @@ -57,7 +56,7 @@ async def list_crowdloans( ) if not loans: if json_output: - json_console.print( + print_json_data( json.dumps( { "success": True, @@ -147,7 +146,7 @@ async def list_crowdloans( "network": subtensor.network, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) return True if not verbose: @@ -338,7 +337,7 @@ async def show_crowdloan_details( if not crowdloan: error_msg = f"Crowdloan #{crowdloan_id} not found." if json_output: - json_console.print(json.dumps({"success": False, "error": error_msg})) + print_json_data({"success": False, "error": error_msg}) else: print_error(f"[red]{error_msg}[/red]") return False, error_msg @@ -437,7 +436,7 @@ async def show_crowdloan_details( "network": subtensor.network, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) return True, f"Displayed info for crowdloan #{crowdloan_id}" table = Table( diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 7997afc5f..31eef42fd 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import TYPE_CHECKING, Optional from async_substrate_interface import AsyncExtrinsicReceipt @@ -11,9 +10,14 @@ unlock_key, console, print_error, - json_console, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import ( + print_transaction_response, + print_json_data, + TransactionResult, + MultiTransactionResult, +) from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src.commands.liquidity.utils import ( LiquidityPosition, @@ -295,13 +299,7 @@ async def add_liquidity( else: ext_id = None if json_output: - json_console.print_json( - data={ - "success": success, - "message": message, - "extrinsic_identifier": ext_id, - } - ) + print_transaction_response(success, message, ext_id) else: if success: console.print( @@ -501,9 +499,7 @@ async def show_liquidity_list( (success, err_msg, positions) = liquidity_list_ if not success: if json_output: - json_console.print( - json.dumps({"success": success, "err_msg": err_msg, "positions": []}) - ) + print_json_data({"success": success, "err_msg": err_msg, "positions": []}) return else: print_error(f"Error: {err_msg}") @@ -558,9 +554,7 @@ async def show_liquidity_list( if not json_output: console.print(liquidity_table) else: - json_console.print( - json.dumps({"success": True, "err_msg": "", "positions": json_table}) - ) + print_json_data({"success": True, "err_msg": "", "positions": json_table}) async def remove_liquidity( @@ -584,9 +578,7 @@ async def remove_liquidity( success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid) if not success: if json_output: - json_console.print_json( - data={"success": False, "err_msg": msg, "positions": positions} - ) + print_json_data({"success": False, "err_msg": msg, "positions": positions}) else: return print_error(f"Error: {msg}") return None @@ -629,14 +621,11 @@ async def remove_liquidity( else: print_error(f"Error removing {posid}: {msg}") else: - json_table = {} + json_results = MultiTransactionResult() for (success, msg, ext_receipt), posid in zip(results, position_ids): - json_table[posid] = { - "success": success, - "err_msg": msg, - "extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(), - } - json_console.print_json(data=json_table) + ext_id = await ext_receipt.get_extrinsic_identifier() if ext_receipt else None + json_results.add(str(posid), success, msg, ext_id) + json_results.print() return None @@ -657,7 +646,7 @@ async def modify_liquidity( if not await subtensor.subnet_exists(netuid=netuid): err_msg = f"Subnet with netuid: {netuid} does not exist in {subtensor}." if json_output: - json_console.print(json.dumps({"success": False, "err_msg": err_msg})) + print_transaction_response(False, err_msg, None) else: print_error(err_msg) return False @@ -687,9 +676,7 @@ async def modify_liquidity( ) if json_output: ext_id = await ext_receipt.get_extrinsic_identifier() if success else None - json_console.print_json( - data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id} - ) + print_transaction_response(success, msg, ext_id) else: if success: await print_extrinsic_id(ext_receipt) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 8852fedf1..b9c560607 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -10,7 +10,6 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, print_extrinsic_id, - json_console, console, print_error, print_success, @@ -18,6 +17,10 @@ ProxyAddressBook, is_valid_ss58_address_prompt, ) +from bittensor_cli.src.bittensor.json_utils import ( + print_transaction_response, + print_transaction_with_data, +) if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -87,25 +90,13 @@ async def submit_proxy( ) if success: if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "extrinsic_identifier": await receipt.get_extrinsic_identifier(), - } - ) + print_transaction_response(success, msg, await receipt.get_extrinsic_identifier()) else: await print_extrinsic_id(receipt) print_success("Success!") else: if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "extrinsic_identifier": None, - } - ) + print_transaction_response(success, msg) else: print_error(f"Failed: {msg}") @@ -146,13 +137,7 @@ async def create_proxy( if not json_output: print_error(ulw.message) else: - json_console.print_json( - data={ - "success": ulw.success, - "message": ulw.message, - "extrinsic_identifier": None, - } - ) + print_transaction_response(ulw.success, ulw.message) return None call = await subtensor.substrate.compose_call( call_module="Proxy", @@ -187,18 +172,16 @@ async def create_proxy( ) if json_output: - json_console.print_json( + print_transaction_with_data( + success, + msg, + await receipt.get_extrinsic_identifier(), data={ - "success": success, - "message": msg, - "data": { - "pure": created_pure, - "spawner": created_spawner, - "proxy_type": created_proxy_type.value, - "delay": delay, - }, - "extrinsic_identifier": await receipt.get_extrinsic_identifier(), - } + "pure": created_pure, + "spawner": created_spawner, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, ) else: await print_extrinsic_id(receipt) @@ -239,14 +222,7 @@ async def create_proxy( return None else: if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "data": None, - "extrinsic_identifier": None, - } - ) + print_transaction_with_data(success, msg, None, data=None) else: print_error(f"Failed to create pure proxy: {msg}") return None @@ -281,13 +257,7 @@ async def remove_proxy( if not json_output: print_error(ulw.message) else: - json_console.print_json( - data={ - "success": ulw.success, - "message": ulw.message, - "extrinsic_identifier": None, - } - ) + print_transaction_response(ulw.success, ulw.message) return None call = await subtensor.substrate.compose_call( call_module="Proxy", @@ -346,13 +316,7 @@ async def add_proxy( if not json_output: print_error(ulw.message) else: - json_console.print_json( - data={ - "success": ulw.success, - "message": ulw.message, - "extrinsic_identifier": None, - } - ) + print_transaction_response(ulw.success, ulw.message) return None call = await subtensor.substrate.compose_call( call_module="Proxy", @@ -389,18 +353,16 @@ async def add_proxy( ) if json_output: - json_console.print_json( + print_transaction_with_data( + success, + msg, + await receipt.get_extrinsic_identifier(), data={ - "success": success, - "message": msg, - "data": { - "delegatee": delegatee, - "delegator": delegator, - "proxy_type": created_proxy_type.value, - "delay": delay, - }, - "extrinsic_identifier": await receipt.get_extrinsic_identifier(), - } + "delegatee": delegatee, + "delegator": delegator, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, ) else: await print_extrinsic_id(receipt) @@ -440,14 +402,7 @@ async def add_proxy( ) else: if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "data": None, - "extrinsic_identifier": None, - } - ) + print_transaction_with_data(success, msg, None, data=None) else: print_error(f"Failed to add proxy: {msg}") return None @@ -485,13 +440,7 @@ async def kill_proxy( if not json_output: print_error(ulw.message) else: - json_console.print_json( - data={ - "success": ulw.success, - "message": ulw.message, - "extrinsic_identifier": None, - } - ) + print_transaction_response(ulw.success, ulw.message) return None spawner = spawner or wallet.coldkeypub.ss58_address call = await subtensor.substrate.compose_call( @@ -651,13 +600,10 @@ async def execute_announced( "You should rerun this command on an archive node endpoint." ) if json_output: - json_console.print_json( - data={ - "success": False, - "message": f"Unable to regenerate the call data using the latest runtime: {e}. " - "You should rerun this command on an archive node endpoint.", - "extrinsic_identifier": None, - } + print_transaction_response( + False, + f"Unable to regenerate the call data using the latest runtime: {e}. " + "You should rerun this command on an archive node endpoint.", ) return False @@ -680,21 +626,13 @@ async def execute_announced( ) if success is True: if json_output: - json_console.print_json( - data={ - "success": True, - "message": msg, - "extrinsic_identifier": await receipt.get_extrinsic_identifier(), - } - ) + print_transaction_response(True, msg, await receipt.get_extrinsic_identifier()) else: print_success("Success!") await print_extrinsic_id(receipt) else: if json_output: - json_console.print_json( - data={"success": False, "message": msg, "extrinsic_identifier": None} - ) + print_transaction_response(False, msg) else: print_error(f"Failed. {msg} ") return success diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 275049be4..d67b7f607 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -23,10 +23,10 @@ print_success, print_verbose, unlock_key, - json_console, get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data from bittensor_wallet import Wallet if TYPE_CHECKING: @@ -527,13 +527,11 @@ async def stake_extrinsic( staking_address ] = await ext_receipt.get_extrinsic_identifier() if json_output: - json_console.print_json( - data={ - "staking_success": successes, - "error_messages": error_messages, - "extrinsic_ids": extrinsic_ids, - } - ) + print_json_data({ + "staking_success": successes, + "error_messages": error_messages, + "extrinsic_ids": extrinsic_ids, + }) # Helper functions diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 3d7888321..a77ad2e56 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import Optional, TYPE_CHECKING from bittensor_wallet import Wallet @@ -10,7 +9,6 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, - json_console, print_success, get_subnet_name, is_valid_ss58_address, @@ -18,6 +16,7 @@ unlock_key, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data, print_transaction_with_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -124,7 +123,7 @@ def resolve_identity(hotkey: str) -> Optional[str]: ) if json_output: - json_console.print(json.dumps(data_output)) + print_json_data(data_output) return data_output table = Table( @@ -283,16 +282,12 @@ async def set_auto_stake_destination( ext_id = await ext_receipt.get_extrinsic_identifier() if success else None if json_output: - json_console.print( - json.dumps( - { - "success": success, - "error": error_message, - "netuid": netuid, - "hotkey": hotkey_ss58, - "extrinsic_identifier": ext_id, - } - ) + print_transaction_with_data( + success=success, + message=error_message, + extrinsic_identifier=ext_id, + netuid=netuid, + hotkey=hotkey_ss58, ) if success: diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 69d4083e4..559868e1a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -22,10 +22,10 @@ is_valid_ss58_address, format_error_message, unlock_key, - json_console, get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data async def get_childkey_completion_block( @@ -607,7 +607,7 @@ async def set_children( ) print_success("Sent set children request for all subnets.") if json_output: - json_console.print(json.dumps(successes)) + print_json_data(successes) async def revoke_children( @@ -698,7 +698,7 @@ async def revoke_children( f"Childkey revocation failed for netuid {netuid_}: {message}." ) if json_output: - json_console.print(json.dumps(dict_output)) + print_json_data(dict_output) async def childkey_take( diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 2f225f135..941c380d5 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,5 +1,4 @@ import asyncio -import json from enum import Enum from typing import TYPE_CHECKING, Optional @@ -18,11 +17,15 @@ print_success, unlock_key, print_extrinsic_id, - json_console, millify_tao, group_subnets, parse_subnet_range, ) +from bittensor_cli.src.bittensor.json_utils import ( + print_json_data, + print_transaction_response, + print_transaction_with_data, +) if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -87,7 +90,7 @@ async def set_claim_type( msg = f"Invalid netuid format: {e}" print_error(msg) if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) + print_transaction_response(False, msg) return False, msg, None claim_table = Table( @@ -115,15 +118,7 @@ async def set_claim_type( msg = "Operation cancelled." console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - } - ) - ) + print_transaction_response(False, msg, None) return False, msg, None # Keep netuids passed thru the cli and assume Keep type @@ -143,7 +138,7 @@ async def set_claim_type( msg = f"Invalid subnets (not available): {group_subnets(invalid)}" print_error(msg) if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) + print_transaction_response(False, msg) return False, msg, None if not keep_subnets: @@ -160,15 +155,7 @@ async def set_claim_type( msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. \nNo change needed." console.print(msg) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": None, - } - ) - ) + print_transaction_response(True, msg, None) return True, msg, None if prompt: @@ -186,14 +173,14 @@ async def set_claim_type( msg = "Operation cancelled." console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) + print_transaction_response(False, msg) return False, msg, None if not (unlock := unlock_key(wallet)).success: msg = f"Failed to unlock wallet: {unlock.message}" print_error(msg) if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) + print_transaction_response(False, msg) return False, msg, None with console.status(":satellite: Setting root claim type...", spinner="earth"): @@ -213,21 +200,13 @@ async def set_claim_type( print_success(msg) await print_extrinsic_id(ext_receipt) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": ext_id, - } - ) - ) + print_transaction_response(True, msg, ext_id) return True, msg, ext_id else: msg = f"Failed to set claim type: {err_msg}" print_error(msg) if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) + print_transaction_response(False, msg) return False, msg, None @@ -256,16 +235,7 @@ async def process_pending_claims( msg = "No stakes found for this coldkey" console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": None, - "netuids": [], - } - ) - ) + print_transaction_with_data(True, msg, None, netuids=[]) return True, msg, None current_stakes = { @@ -317,16 +287,7 @@ async def process_pending_claims( msg = "No claimable emissions found" console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": None, - "netuids": netuids, - } - ) - ) + print_transaction_with_data(True, msg, None, netuids=netuids) return True, msg, None _print_claimable_table(wallet, claimable_stake_info, verbose) @@ -352,32 +313,14 @@ async def process_pending_claims( msg = "Operation cancelled by user" console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "netuids": selected_netuids, - } - ) - ) + print_transaction_with_data(False, msg, None, netuids=selected_netuids) return False, msg, None if not (unlock := unlock_key(wallet)).success: msg = f"Failed to unlock wallet: {unlock.message}" print_error(msg) if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "netuids": selected_netuids, - } - ) - ) + print_transaction_with_data(False, msg, None, netuids=selected_netuids) return False, msg, None with console.status( @@ -393,31 +336,13 @@ async def process_pending_claims( console.print(f"[dark_sea_green3]{msg}[/dark_sea_green3]") await print_extrinsic_id(ext_receipt) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": ext_id, - "netuids": selected_netuids, - } - ) - ) + print_transaction_with_data(True, msg, ext_id, netuids=selected_netuids) return True, msg, ext_id else: msg = f"Failed to claim root emissions: {err_msg}" print_error(msg) if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "netuids": selected_netuids, - } - ) - ) + print_transaction_with_data(False, msg, None, netuids=selected_netuids) return False, msg, None diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index a2a554578..8c6534de1 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -19,8 +19,8 @@ print_error, millify_tao, get_subnet_name, - json_console, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -651,7 +651,7 @@ def format_cell( dict_output["total_tao_value"] = all_hks_tao_value.tao + balance.tao dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao if json_output: - json_console.print(json.dumps(dict_output)) + print_json_data(dict_output) if not sub_stakes: console.print( f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index eb930a60e..84bb1b212 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -1,5 +1,4 @@ import asyncio -import json from functools import partial from typing import TYPE_CHECKING, Optional @@ -26,10 +25,10 @@ format_error_message, group_subnets, unlock_key, - json_console, get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -371,7 +370,7 @@ async def unstake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) if json_output: - json_console.print_json(data=successes) + print_json_data(successes) return True @@ -581,7 +580,7 @@ async def unstake_all( "extrinsic_identifier": ext_id, } if json_output: - json_console.print(json.dumps({"success": successes})) + print_json_data({"success": successes}) # Extrinsics diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index 195820c56..50fd516fb 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -13,11 +13,11 @@ confirm_action, console, print_error, - json_console, U16_MAX, print_extrinsic_id, print_success, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -34,8 +34,7 @@ async def count( if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): print_error(f"Subnet {netuid} does not exist") if json_output: - json_console.print_json( - data={"success": False, "error": f"Subnet {netuid} does not exist"} + print_json_data({"success": False, "error": f"Subnet {netuid} does not exist"} ) return None @@ -48,13 +47,11 @@ async def count( ) if not mechanism_count: if json_output: - json_console.print_json( - data={ - "netuid": netuid, - "count": None, - "error": "Failed to get mechanism count", - } - ) + print_json_data({ + "netuid": netuid, + "count": None, + "error": "Failed to get mechanism count", + }) else: print_error( "Subnet mechanism count: [red]Failed to get mechanism count[/red]" @@ -62,13 +59,11 @@ async def count( return None if json_output: - json_console.print_json( - data={ - "netuid": netuid, - "count": mechanism_count, - "error": "", - } - ) + print_json_data({ + "netuid": netuid, + "count": mechanism_count, + "error": "", + }) else: console.print( f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism" @@ -92,8 +87,7 @@ async def get_emission_split( f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." ) if json_output: - json_console.print_json( - data={ + print_json_data({ "success": False, "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", } @@ -128,7 +122,7 @@ async def get_emission_split( } if json_output: - json_console.print_json(data=data) + print_json_data(data) else: table = Table( Column( @@ -204,7 +198,7 @@ async def set_emission_split( f"Subnet {netuid} does not currently contain any mechanisms to configure." ) if json_output: - json_console.print_json(data={"success": False, "error": message}) + print_json_data({"success": False, "error": message}) else: print_error(message) return False @@ -232,7 +226,7 @@ async def set_emission_split( "Invalid `--split` values. Provide a comma-separated list of numbers." ) if json_output: - json_console.print_json(data={"success": False, "error": message}) + print_json_data({"success": False, "error": message}) else: print_error(message) return False @@ -271,7 +265,7 @@ async def set_emission_split( if len(weights) != mech_count: message = f"Expected {mech_count} weight values, received {len(weights)}." if json_output: - json_console.print_json(data={"success": False, "error": message}) + print_json_data({"success": False, "error": message}) else: print_error(message) return False @@ -279,7 +273,7 @@ async def set_emission_split( if any(value < 0 for value in weights): message = "Weights must be non-negative." if json_output: - json_console.print_json(data={"success": False, "error": message}) + print_json_data({"success": False, "error": message}) else: print_error(message) return False @@ -289,7 +283,7 @@ async def set_emission_split( except ValueError as exc: message = str(exc) if json_output: - json_console.print_json(data={"success": False, "error": message}) + print_json_data({"success": False, "error": message}) else: print_error(message) return False @@ -297,8 +291,7 @@ async def set_emission_split( if normalized_weights == existing_split: message = "[dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" if json_output: - json_console.print_json( - data={ + print_json_data({ "success": True, "message": "Emission split unchanged.", "split": normalized_weights, @@ -373,15 +366,13 @@ async def set_emission_split( ) if json_output: - json_console.print_json( - data={ - "success": success, - "err_msg": err_msg, - "split": normalized_weights, - "percentages": [round(value * 100, 6) for value in fractions], - "extrinsic_identifier": ext_id, - } - ) + print_json_data({ + "success": success, + "err_msg": err_msg, + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + "extrinsic_identifier": ext_id, + }) return success diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 7f886f2be..c348e292c 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -1,5 +1,4 @@ import asyncio -import json import math import tempfile import webbrowser @@ -15,9 +14,9 @@ console, get_subnet_name, print_error, - json_console, jinja_env, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -90,7 +89,7 @@ async def price( subnet_data, block_numbers, interval_hours, log_scale ) elif json_output: - json_console.print(json.dumps(_generate_json_output(subnet_data))) + print_json_data(_generate_json_output(subnet_data)) else: _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) else: @@ -103,7 +102,7 @@ async def price( all_subnet_info, netuids, all_netuids ) if json_output: - json_console.print(json.dumps(_generate_json_output(subnet_data))) + print_json_data(_generate_json_output(subnet_data)) else: _generate_cli_output_current(subnet_data) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 26c662173..b26e757e1 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -41,11 +41,11 @@ get_subnet_name, unlock_key, blocks_to_duration, - json_console, get_hotkey_pub_ss58, print_extrinsic_id, check_img_mimetype, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -982,7 +982,7 @@ def format_liquidity_cell( # Non-live mode subnets, block_number, mechanisms, ema_tao_inflow = await fetch_subnet_data() if json_output: - json_console.print( + print_json_data( json.dumps( dict_table(subnets, block_number, mechanisms, ema_tao_inflow) ) @@ -1601,7 +1601,7 @@ async def show_subnet( "uids": json_out_rows, } if json_output: - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) mech_line = ( f"\n Mechanism ID: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]#{selected_mechanism_id}" @@ -1699,7 +1699,7 @@ async def burn_cost( current_burn_cost = await subtensor.burn_cost() if current_burn_cost: if json_output: - json_console.print( + print_json_data( json.dumps({"burn_cost": current_burn_cost.to_dict(), "error": ""}) ) else: @@ -1709,7 +1709,7 @@ async def burn_cost( return current_burn_cost else: if json_output: - json_console.print( + print_json_data( json.dumps( {"burn_cost": None, "error": "Failed to get subnet burn cost"} ) @@ -1746,7 +1746,7 @@ async def create( if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present # (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid. - json_console.print( + print_json_data( json.dumps( {"success": success, "netuid": netuid, "extrinsic_identifier": ext_id} ) @@ -1860,7 +1860,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): print_error(f"Subnet {netuid} does not exist") if json_output: - json_console.print_json( + print_json_data( data={ "success": False, "msg": f"Subnet {netuid} does not exist", @@ -1886,7 +1886,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: err_msg = f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO" print_error(err_msg) if json_output: - json_console.print_json( + print_json_data( data={"success": False, "msg": err_msg, "extrinsic_identifier": None} ) return @@ -1996,7 +1996,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: if not registration_allowed: print_error(f"Registration to subnet {netuid} is not allowed") if json_output: - json_console.print_json( + print_json_data( data={ "success": False, "msg": f"Registration to subnet {netuid} is not allowed", @@ -2013,7 +2013,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: f"Try again in {remaining_blocks} blocks." ) if json_output: - json_console.print_json( + print_json_data( data={ "success": False, "msg": f"Registration to subnet {netuid} is full for this interval. " @@ -2023,7 +2023,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: ) return if json_output: - json_console.print( + print_json_data( json.dumps({"success": success, "msg": msg, "extrinsic_identifier": ext_id}) ) @@ -2679,7 +2679,7 @@ async def get_identity( f" on {subtensor}" ) if json_output: - json_console.print("{}") + print_json_data("{}") return {} else: table = create_identity_table(title=title) @@ -2699,7 +2699,7 @@ async def get_identity( table.add_row(key, str(value) if value else "~") dict_out[key] = value if json_output: - json_console.print(json.dumps(dict_out)) + print_json_data(dict_out) else: console.print(table) return identity @@ -2845,7 +2845,7 @@ async def set_symbol( if not await subtensor.subnet_exists(netuid): err = f"Subnet {netuid} does not exist." if json_output: - json_console.print_json( + print_json_data( data={"success": False, "message": err, "extrinsic_identifier": None} ) else: @@ -2864,7 +2864,7 @@ async def set_symbol( if not (unlock_status := unlock_key(wallet, print_out=False)).success: err = unlock_status.message if json_output: - json_console.print_json(data={"success": False, "message": err}) + print_json_data({"success": False, "message": err}) else: console.print(err) return False @@ -2884,7 +2884,7 @@ async def set_symbol( await print_extrinsic_id(response) message = f"Successfully updated SN{netuid}'s symbol to {symbol}." if json_output: - json_console.print_json( + print_json_data( data={ "success": True, "message": message, @@ -2896,7 +2896,7 @@ async def set_symbol( return True else: if json_output: - json_console.print_json( + print_json_data( data={ "success": False, "message": err_msg, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index ec6d1461c..3ee0010c8 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -28,12 +28,12 @@ normalize_hyperparameters, unlock_key, blocks_to_duration, - json_console, string_to_u16, string_to_u64, get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_json_data, print_transaction_response if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import ( @@ -1008,7 +1008,7 @@ async def get_senate( ) dict_output.append({"name": member_name, "ss58_address": ss58_address}) if json_output: - json_console.print(json.dumps(dict_output, ensure_ascii=True)) + print_json_data(dict_output) return console.print(table) @@ -1101,7 +1101,7 @@ async def proposals( } ) if json_output: - json_console.print(json.dumps(dict_output, ensure_ascii=True)) + print_json_data(dict_output) console.print(table) console.print( "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]" @@ -1249,7 +1249,7 @@ async def trim( if subnet_owner != wallet.coldkeypub.ss58_address: err_msg = "This wallet doesn't own the specified subnet." if json_output: - json_console.print_json(data={"success": False, "message": err_msg}) + print_transaction_response(False, err_msg, None) else: print_error(err_msg) return False @@ -1271,13 +1271,7 @@ async def trim( ) if not success: if json_output: - json_console.print_json( - data={ - "success": False, - "message": err_msg, - "extrinsic_identifier": None, - } - ) + print_transaction_response(False, err_msg, None) else: print_error(err_msg) return False @@ -1285,9 +1279,7 @@ async def trim( ext_id = await ext_receipt.get_extrinsic_identifier() msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}" if json_output: - json_console.print_json( - data={"success": True, "message": msg, "extrinsic_identifier": ext_id} - ) + print_transaction_response(True, msg, ext_id) else: await print_extrinsic_id(ext_receipt) print_success(f"[dark_sea_green3]{msg}[/dark_sea_green3]") diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 216d7bf10..a147d4ded 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -37,7 +37,6 @@ confirm_action, console, convert_blocks_to_time, - json_console, print_error, print_verbose, get_all_wallets_for_path, @@ -53,6 +52,13 @@ get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import ( + json_success, + json_error, + print_json, + print_json_data, + print_transaction_response, +) class SortByBalance(Enum): @@ -181,33 +187,21 @@ async def regen_coldkey( f"coldkey ss58: ({new_wallet.coldkeypub.ss58_address})", ) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "data": { - "name": new_wallet.name, - "path": new_wallet.path, - "hotkey": new_wallet.hotkey_str, - "hotkey_ss58": get_hotkey_pub_ss58(new_wallet), - "coldkey_ss58": new_wallet.coldkeypub.ss58_address, - }, - "error": "", - } - ) - ) + print_json(json_success({ + "name": new_wallet.name, + "path": new_wallet.path, + "hotkey": new_wallet.hotkey_str, + "hotkey_ss58": get_hotkey_pub_ss58(new_wallet), + "coldkey_ss58": new_wallet.coldkeypub.ss58_address, + })) except ValueError: print_error("Mnemonic phrase is invalid") if json_output: - json_console.print( - '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' - ) + print_json(json_error("Mnemonic phrase is invalid")) except KeyFileError: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( - '{"success": false, "error": "Keyfile is not writable", "data": null}' - ) + print_json(json_error("Keyfile is not writable")) async def regen_coldkey_pub( @@ -231,27 +225,17 @@ async def regen_coldkey_pub( f"coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})", ) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "data": { - "name": new_coldkeypub.name, - "path": new_coldkeypub.path, - "hotkey": new_coldkeypub.hotkey_str, - "hotkey_ss58": get_hotkey_pub_ss58(new_coldkeypub), - "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address, - }, - "error": "", - } - ) - ) + print_json(json_success({ + "name": new_coldkeypub.name, + "path": new_coldkeypub.path, + "hotkey": new_coldkeypub.hotkey_str, + "hotkey_ss58": get_hotkey_pub_ss58(new_coldkeypub), + "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address, + })) except KeyFileError: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( - '{"success": false, "error": "Keyfile is not writable", "data": null}' - ) + print_json(json_error("Keyfile is not writable")) async def regen_hotkey( @@ -288,33 +272,21 @@ async def regen_hotkey( f"hotkey ss58: ({new_hotkey_.hotkeypub.ss58_address})", ) if json_output: - json_console.print( - json.dumps( - { - "success": True, - "data": { - "name": new_hotkey_.name, - "path": new_hotkey_.path, - "hotkey": new_hotkey_.hotkey_str, - "hotkey_ss58": new_hotkey_.hotkeypub.ss58_address, - "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address, - }, - "error": "", - } - ) - ) + print_json(json_success({ + "name": new_hotkey_.name, + "path": new_hotkey_.path, + "hotkey": new_hotkey_.hotkey_str, + "hotkey_ss58": new_hotkey_.hotkeypub.ss58_address, + "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address, + })) except ValueError: print_error("Mnemonic phrase is invalid") if json_output: - json_console.print( - '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' - ) + print_json(json_error("Mnemonic phrase is invalid")) except KeyFileError: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( - '{"success": false, "error": "Keyfile is not writable", "data": null}' - ) + print_json(json_error("Keyfile is not writable")) async def regen_hotkey_pub( @@ -338,7 +310,7 @@ async def regen_hotkey_pub( f"coldkey ss58: ({new_hotkeypub.coldkeypub.ss58_address})", ) if json_output: - json_console.print( + print_json_data( json.dumps( { "success": True, @@ -356,7 +328,7 @@ async def regen_hotkey_pub( except KeyFileError: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( + print_json_data( '{"success": false, "error": "Keyfile is not writable", "data": null}' ) @@ -389,7 +361,7 @@ async def new_hotkey( ) console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") if json_output: - json_console.print( + print_json_data( json.dumps( { "success": True, @@ -407,7 +379,7 @@ async def new_hotkey( except KeyFileError: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( + print_json_data( '{"success": false, "error": "Keyfile is not writable", "data": null}' ) @@ -440,7 +412,7 @@ async def new_coldkey( ) console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") if json_output: - json_console.print( + print_json_data( json.dumps( { "success": True, @@ -456,7 +428,7 @@ async def new_coldkey( except KeyFileError as e: print_error("KeyFileError: File is not writable") if json_output: - json_console.print( + print_json_data( json.dumps( { "success": False, @@ -543,7 +515,7 @@ async def wallet_create( print_error(err) output_dict["error"] = err if json_output: - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: @@ -721,7 +693,7 @@ async def wallet_balance( "total": (total_free_balance + total_staked_balance).tao, }, } - json_console.print(json.dumps(output_dict)) + print_json_data(output_dict) return total_free_balance @@ -930,7 +902,7 @@ async def wallet_list( ) root.add(message) if json_output: - json_console.print(json.dumps(main_data_dict)) + print_json_data(main_data_dict) else: console.print(root) @@ -1363,7 +1335,7 @@ def overview_sort_function(row_): if not json_output: console.print(grid, width=None) else: - json_console.print(json.dumps(data_dict)) + print_json_data(data_dict) def _get_hotkeys( @@ -1551,9 +1523,8 @@ async def transfer( ) ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: - json_console.print( - json.dumps({"success": result, "extrinsic_identifier": ext_id}) - ) + msg = "Transfer successful" if result else "Transfer failed" + print_transaction_response(result, msg, ext_id) else: await print_extrinsic_id(ext_receipt) return result @@ -1759,9 +1730,8 @@ async def swap_hotkey( else: ext_id = None if json_output: - json_console.print( - json.dumps({"success": result, "extrinsic_identifier": ext_id}) - ) + msg = "Hotkey swap successful" if result else "Hotkey swap failed" + print_transaction_response(result, msg, ext_id) else: await print_extrinsic_id(ext_receipt) return result @@ -1837,23 +1807,20 @@ async def set_id( print_error(f"Failed! {err_msg}") output_dict["error"] = err_msg if json_output: - json_console.print(json.dumps(output_dict)) + print_transaction_response(False, err_msg, None) return False else: console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") ext_id = await ext_receipt.get_extrinsic_identifier() await print_extrinsic_id(ext_receipt) - output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.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 else "~") - output_dict["identity"] = identity - output_dict["extrinsic_identifier"] = ext_id if json_output: - json_console.print(json.dumps(output_dict)) + print_transaction_response(True, "Identity set successfully", ext_id) else: console.print(table) return True @@ -1877,7 +1844,7 @@ async def get_id( f" on {subtensor}" ) if json_output: - json_console.print("{}") + print_json_data("{}") return {} table = create_identity_table(title) @@ -1887,7 +1854,7 @@ async def get_id( console.print(table) if json_output: - json_console.print(json.dumps(identity)) + print_json_data(identity) return identity @@ -1953,7 +1920,7 @@ async def sign( console.print("[dark_sea_green3]Message signed successfully!\n") if json_output: - json_console.print( + print_json_data( json.dumps( {"signed_message": signed_message, "signer_address": signer_address} ) @@ -1993,7 +1960,7 @@ async def verify( except (ValueError, TypeError) as e: if json_output: - json_console.print( + print_json_data( json.dumps( { "verified": False, @@ -2011,7 +1978,7 @@ async def verify( signature_bytes = bytes.fromhex(signature.strip().lower().replace("0x", "")) except ValueError as e: if json_output: - json_console.print( + print_json_data( json.dumps( { "verified": False, @@ -2026,7 +1993,7 @@ async def verify( is_valid = keypair.verify(message.encode("utf-8"), signature_bytes) if json_output: - json_console.print( + print_json_data( json.dumps( {"verified": is_valid, "signer": signer_address, "message": message} ) diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 3fa0135c3..717cbbd98 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -15,10 +15,10 @@ print_success, console, format_error_message, - json_console, get_hotkey_pub_ss58, print_extrinsic_id, ) +from bittensor_cli.src.bittensor.json_utils import print_transaction_response from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, generate_weight_hash, @@ -405,11 +405,7 @@ async def reveal_weights( ) success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals) if json_output: - json_console.print( - json.dumps( - {"success": success, "message": message, "extrinsic_identifier": ext_id} - ) - ) + print_transaction_response(success, message, ext_id) else: if success: console.print("Weights revealed successfully") @@ -455,11 +451,7 @@ async def commit_weights( ) success, message, ext_id = await extrinsic.set_weights_extrinsic() if json_output: - json_console.print( - json.dumps( - {"success": success, "message": message, "extrinsic_identifier": ext_id} - ) - ) + print_transaction_response(success, message, ext_id) else: if success: console.print("Weights set successfully") diff --git a/tests/unit_tests/test_json_utils.py b/tests/unit_tests/test_json_utils.py new file mode 100644 index 000000000..277ef69a8 --- /dev/null +++ b/tests/unit_tests/test_json_utils.py @@ -0,0 +1,315 @@ +""" +Unit tests for JSON output utilities. + +Tests the standardized JSON response formatting used across btcli commands. +""" + +import json +import pytest +from io import StringIO +from unittest.mock import patch + +from bittensor_cli.src.bittensor.json_utils import ( + json_response, + json_success, + json_error, + serialize_balance, + transaction_response, + TransactionResult, + MultiTransactionResult, +) + + +class TestJsonResponse: + """Tests for the json_response function.""" + + def test_success_with_data(self): + """Test successful response with data.""" + result = json_response(success=True, data={"key": "value"}) + parsed = json.loads(result) + + assert parsed["success"] is True + assert parsed["data"] == {"key": "value"} + assert "error" not in parsed + + def test_success_without_data(self): + """Test successful response without data.""" + result = json_response(success=True) + parsed = json.loads(result) + + assert parsed["success"] is True + assert "data" not in parsed + assert "error" not in parsed + + def test_error_response(self): + """Test error response.""" + result = json_response(success=False, error="Something went wrong") + parsed = json.loads(result) + + assert parsed["success"] is False + assert parsed["error"] == "Something went wrong" + + def test_error_with_data(self): + """Test error response with additional data.""" + result = json_response( + success=False, + data={"partial": "data"}, + error="Partial failure" + ) + parsed = json.loads(result) + + assert parsed["success"] is False + assert parsed["data"] == {"partial": "data"} + assert parsed["error"] == "Partial failure" + + def test_nested_data(self): + """Test response with nested data structures.""" + data = { + "wallet": { + "name": "test", + "hotkeys": ["hk1", "hk2"], + "balance": {"rao": 1000000000, "tao": 1.0} + } + } + result = json_response(success=True, data=data) + parsed = json.loads(result) + + assert parsed["success"] is True + assert parsed["data"]["wallet"]["name"] == "test" + assert len(parsed["data"]["wallet"]["hotkeys"]) == 2 + + +class TestJsonSuccess: + """Tests for the json_success helper.""" + + def test_simple_data(self): + """Test success with simple data.""" + result = json_success({"status": "ok"}) + parsed = json.loads(result) + + assert parsed["success"] is True + assert parsed["data"]["status"] == "ok" + + def test_list_data(self): + """Test success with list data.""" + result = json_success([1, 2, 3]) + parsed = json.loads(result) + + assert parsed["success"] is True + assert parsed["data"] == [1, 2, 3] + + def test_primitive_data(self): + """Test success with primitive data.""" + result = json_success("simple string") + parsed = json.loads(result) + + assert parsed["success"] is True + assert parsed["data"] == "simple string" + + +class TestJsonError: + """Tests for the json_error helper.""" + + def test_simple_error(self): + """Test simple error message.""" + result = json_error("Connection failed") + parsed = json.loads(result) + + assert parsed["success"] is False + assert parsed["error"] == "Connection failed" + assert "data" not in parsed + + def test_error_with_context(self): + """Test error with additional context data.""" + result = json_error("Validation failed", data={"field": "amount"}) + parsed = json.loads(result) + + assert parsed["success"] is False + assert parsed["error"] == "Validation failed" + assert parsed["data"]["field"] == "amount" + + +class TestTransactionResponse: + """Tests for the transaction_response helper.""" + + def test_successful_transaction(self): + """Test successful transaction response.""" + result = transaction_response( + success=True, + message="Transfer successful", + extrinsic_identifier="12345678-2" + ) + + assert result["success"] is True + assert result["message"] == "Transfer successful" + assert result["extrinsic_identifier"] == "12345678-2" + + def test_failed_transaction(self): + """Test failed transaction response.""" + result = transaction_response( + success=False, + message="Insufficient balance" + ) + + assert result["success"] is False + assert result["message"] == "Insufficient balance" + assert result["extrinsic_identifier"] is None + + def test_transaction_without_message(self): + """Test transaction without message.""" + result = transaction_response( + success=True, + extrinsic_identifier="12345678-3" + ) + + assert result["success"] is True + assert result["message"] is None + assert result["extrinsic_identifier"] == "12345678-3" + + +class TestTransactionResult: + """Tests for the TransactionResult class.""" + + def test_as_dict(self): + """Test conversion to dictionary.""" + result = TransactionResult( + success=True, + message="Success", + extrinsic_identifier="12345-1" + ) + d = result.as_dict() + + assert d["success"] is True + assert d["message"] == "Success" + assert d["extrinsic_identifier"] == "12345-1" + + def test_failed_result(self): + """Test failed transaction result.""" + result = TransactionResult( + success=False, + message="Error occurred" + ) + d = result.as_dict() + + assert d["success"] is False + assert d["message"] == "Error occurred" + assert d["extrinsic_identifier"] is None + + +class TestMultiTransactionResult: + """Tests for the MultiTransactionResult class.""" + + def test_add_results(self): + """Test adding multiple results.""" + multi = MultiTransactionResult() + multi.add("pos1", True, "Success 1", "12345-1") + multi.add("pos2", False, "Failed 2", None) + + d = multi.as_dict() + + assert "pos1" in d + assert "pos2" in d + assert d["pos1"]["success"] is True + assert d["pos1"]["extrinsic_identifier"] == "12345-1" + assert d["pos2"]["success"] is False + assert d["pos2"]["extrinsic_identifier"] is None + + def test_add_result_object(self): + """Test adding TransactionResult objects.""" + multi = MultiTransactionResult() + result = TransactionResult(True, "Test", "123-1") + multi.add_result("key1", result) + + d = multi.as_dict() + assert d["key1"]["success"] is True + assert d["key1"]["message"] == "Test" + + +class TestSerializeBalance: + """Tests for balance serialization.""" + + def test_balance_object(self): + """Test serializing a Balance-like object.""" + class MockBalance: + rao = 1000000000 + tao = 1.0 + + result = serialize_balance(MockBalance()) + + assert result["rao"] == 1000000000 + assert result["tao"] == 1.0 + + def test_float_value(self): + """Test serializing a float (assumes TAO).""" + result = serialize_balance(2.5) + + assert result["tao"] == 2.5 + assert result["rao"] == 2500000000 + + def test_int_value(self): + """Test serializing an int (assumes RAO).""" + result = serialize_balance(5000000000) + + assert result["rao"] == 5000000000 + assert result["tao"] == 5.0 + + def test_unknown_type(self): + """Test serializing unknown type returns zeros.""" + result = serialize_balance("invalid") + + assert result["rao"] == 0 + assert result["tao"] == 0.0 + + +class TestJsonOutputConsistency: + """Tests to verify JSON output schema consistency.""" + + def test_success_always_has_success_field(self): + """Verify success responses always include 'success' field.""" + responses = [ + json_success({}), + json_success([]), + json_success("test"), + json_success(None), + ] + + for response in responses: + parsed = json.loads(response) + assert "success" in parsed + assert parsed["success"] is True + + def test_error_always_has_success_and_error_fields(self): + """Verify error responses always include required fields.""" + responses = [ + json_error("error1"), + json_error("error2", data={}), + ] + + for response in responses: + parsed = json.loads(response) + assert "success" in parsed + assert "error" in parsed + assert parsed["success"] is False + + def test_transaction_response_schema(self): + """Verify transaction responses have consistent schema.""" + success_result = transaction_response(True, "OK", "123-1") + fail_result = transaction_response(False, "Failed", None) + + # Both should have all three keys + for result in [success_result, fail_result]: + assert "success" in result + assert "message" in result + assert "extrinsic_identifier" in result + + def test_json_is_valid(self): + """Verify all outputs are valid JSON.""" + test_cases = [ + json_response(True, {"test": "data"}), + json_success({"nested": {"deep": True}}), + json_error("test error"), + ] + + for output in test_cases: + # Should not raise + json.loads(output)