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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions bittensor_cli/src/bittensor/json_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""
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 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}
39 changes: 15 additions & 24 deletions bittensor_cli/src/commands/liquidity/liquidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
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,
Expand Down Expand Up @@ -295,13 +301,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(
Expand Down Expand Up @@ -558,9 +558,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(
Expand All @@ -584,9 +582,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
Expand Down Expand Up @@ -629,14 +625,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


Expand All @@ -657,7 +650,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
Expand Down Expand Up @@ -687,9 +680,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)
Expand Down
13 changes: 6 additions & 7 deletions bittensor_cli/src/commands/stake/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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:
Expand Down Expand Up @@ -528,13 +529,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
Expand Down
5 changes: 3 additions & 2 deletions bittensor_cli/src/commands/stake/remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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
Expand Down Expand Up @@ -370,7 +371,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


Expand Down Expand Up @@ -580,7 +581,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
Expand Down
Loading