Skip to content

Commit 92b056d

Browse files
feat: Expand JSON utils with TransactionResult classes and extrinsic_identifier
- Add TransactionResult and MultiTransactionResult classes for consistent transaction responses across all commands - Add print_transaction_response() helper function - Update schema to use {success, message, extrinsic_identifier} format - Migrate wallets.py: transfer, swap_hotkey, set_id - Migrate sudo.py: trim command - Migrate stake/add.py and stake/remove.py - Migrate liquidity.py: add_liquidity, remove_liquidity, modify_liquidity - Update tests for new transaction response utilities (25 tests passing) This addresses feedback from @thewhaleking on PR #781 to apply standardized JSON output to all json_console usages with extrinsic_identifier support. Closes #635
1 parent b50d3d6 commit 92b056d

File tree

7 files changed

+264
-135
lines changed

7 files changed

+264
-135
lines changed

bittensor_cli/src/bittensor/json_utils.py

Lines changed: 138 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,147 @@
44
This module provides consistent JSON response formatting across all btcli commands.
55
All JSON outputs should use these utilities to ensure schema compliance.
66
7-
Standard Response Format:
7+
Standard Transaction Response Format:
88
{
9-
"success": bool, # Required: Whether the operation succeeded
10-
"data": {...}, # Optional: Command-specific response data
11-
"error": str # Optional: Error message if success=False
9+
"success": bool, # Required: Whether the operation succeeded
10+
"message": str | None, # Optional: Human-readable message
11+
"extrinsic_identifier": str | None # Optional: Block-extrinsic ID (e.g., "12345-2")
1212
}
1313
14-
For transaction responses, data should include:
14+
Standard Data Response Format:
1515
{
16-
"extrinsic_hash": str, # The transaction hash
17-
"block_hash": str # The block containing the transaction
16+
"success": bool, # Required: Whether the operation succeeded
17+
"data": {...}, # Optional: Command-specific response data
18+
"error": str # Optional: Error message if success=False
1819
}
1920
"""
2021

2122
import json
2223
from typing import Any, Optional, Union
2324
from rich.console import Console
2425

25-
# JSON console for outputting JSON responses
26+
# JSON console for outputting JSON responses (matches existing btcli pattern)
2627
json_console = Console()
2728

2829

30+
# =============================================================================
31+
# Transaction Response Utilities (for commands that submit extrinsics)
32+
# =============================================================================
33+
34+
def transaction_response(
35+
success: bool,
36+
message: Optional[str] = None,
37+
extrinsic_identifier: Optional[str] = None,
38+
) -> dict[str, Any]:
39+
"""
40+
Create a standardized transaction response dictionary.
41+
42+
Args:
43+
success: Whether the transaction succeeded
44+
message: Human-readable status message
45+
extrinsic_identifier: The extrinsic ID (e.g., "12345678-2")
46+
47+
Returns:
48+
Dictionary with standardized transaction format
49+
"""
50+
return {
51+
"success": success,
52+
"message": message,
53+
"extrinsic_identifier": extrinsic_identifier,
54+
}
55+
56+
57+
def print_transaction_response(
58+
success: bool,
59+
message: Optional[str] = None,
60+
extrinsic_identifier: Optional[str] = None,
61+
) -> None:
62+
"""
63+
Print a standardized transaction response as JSON.
64+
65+
Args:
66+
success: Whether the transaction succeeded
67+
message: Human-readable status message
68+
extrinsic_identifier: The extrinsic ID (e.g., "12345678-2")
69+
"""
70+
json_console.print_json(data=transaction_response(success, message, extrinsic_identifier))
71+
72+
73+
class TransactionResult:
74+
"""
75+
Helper class for building transaction responses.
76+
77+
Provides a clean interface for transaction commands that need to
78+
build up response data before printing.
79+
"""
80+
81+
def __init__(
82+
self,
83+
success: bool,
84+
message: Optional[str] = None,
85+
extrinsic_identifier: Optional[str] = None,
86+
):
87+
self.success = success
88+
self.message = message
89+
self.extrinsic_identifier = extrinsic_identifier
90+
91+
def as_dict(self) -> dict[str, Any]:
92+
"""Return the response as a dictionary."""
93+
return transaction_response(
94+
self.success,
95+
self.message,
96+
self.extrinsic_identifier,
97+
)
98+
99+
def print(self) -> None:
100+
"""Print the response as JSON."""
101+
json_console.print_json(data=self.as_dict())
102+
103+
104+
class MultiTransactionResult:
105+
"""
106+
Helper class for commands that process multiple transactions.
107+
108+
Builds a keyed dictionary of transaction results.
109+
"""
110+
111+
def __init__(self):
112+
self._results: dict[str, TransactionResult] = {}
113+
114+
def add(
115+
self,
116+
key: str,
117+
success: bool,
118+
message: Optional[str] = None,
119+
extrinsic_identifier: Optional[str] = None,
120+
) -> None:
121+
"""Add a transaction result with the given key."""
122+
self._results[key] = TransactionResult(success, message, extrinsic_identifier)
123+
124+
def add_result(self, key: str, result: TransactionResult) -> None:
125+
"""Add an existing TransactionResult with the given key."""
126+
self._results[key] = result
127+
128+
def as_dict(self) -> dict[str, dict[str, Any]]:
129+
"""Return all results as a dictionary."""
130+
return {k: v.as_dict() for k, v in self._results.items()}
131+
132+
def print(self) -> None:
133+
"""Print all results as JSON."""
134+
json_console.print_json(data=self.as_dict())
135+
136+
137+
# =============================================================================
138+
# Data Response Utilities (for commands that return data, not transactions)
139+
# =============================================================================
140+
29141
def json_response(
30142
success: bool,
31143
data: Optional[Any] = None,
32144
error: Optional[str] = None,
33145
) -> str:
34146
"""
35-
Create a standardized JSON response string.
147+
Create a standardized JSON response string for data queries.
36148
37149
Args:
38150
success: Whether the operation succeeded
@@ -62,7 +174,7 @@ def json_response(
62174

63175
def json_success(data: Any) -> str:
64176
"""
65-
Create a successful JSON response.
177+
Create a successful JSON response string.
66178
67179
Args:
68180
data: Response data to include
@@ -75,7 +187,7 @@ def json_success(data: Any) -> str:
75187

76188
def json_error(error: str, data: Optional[Any] = None) -> str:
77189
"""
78-
Create an error JSON response.
190+
Create an error JSON response string.
79191
80192
Args:
81193
error: Error message describing what went wrong
@@ -87,43 +199,9 @@ def json_error(error: str, data: Optional[Any] = None) -> str:
87199
return json_response(success=False, data=data, error=error)
88200

89201

90-
def json_transaction(
91-
success: bool,
92-
extrinsic_hash: Optional[str] = None,
93-
block_hash: Optional[str] = None,
94-
error: Optional[str] = None,
95-
**extra_data: Any,
96-
) -> str:
97-
"""
98-
Create a standardized transaction response.
99-
100-
Args:
101-
success: Whether the transaction succeeded
102-
extrinsic_hash: The transaction/extrinsic hash
103-
block_hash: The block hash containing the transaction
104-
error: Error message if transaction failed
105-
**extra_data: Additional transaction-specific data
106-
107-
Returns:
108-
JSON string with transaction details
109-
"""
110-
data: dict[str, Any] = {}
111-
112-
if extrinsic_hash is not None:
113-
data["extrinsic_hash"] = extrinsic_hash
114-
115-
if block_hash is not None:
116-
data["block_hash"] = block_hash
117-
118-
# Add any extra data
119-
data.update(extra_data)
120-
121-
return json_response(success=success, data=data if data else None, error=error)
122-
123-
124202
def print_json(response: str) -> None:
125203
"""
126-
Print a JSON response to the console.
204+
Print a JSON string response to the console.
127205
128206
Args:
129207
response: JSON string to print
@@ -152,6 +230,20 @@ def print_json_error(error: str, data: Optional[Any] = None) -> None:
152230
print_json(json_error(error, data))
153231

154232

233+
def print_json_data(data: Any) -> None:
234+
"""
235+
Print data directly as JSON (for simple data responses).
236+
237+
Args:
238+
data: Data to print as JSON
239+
"""
240+
json_console.print_json(data=data)
241+
242+
243+
# =============================================================================
244+
# Serialization Utilities
245+
# =============================================================================
246+
155247
def serialize_balance(balance: Any) -> dict[str, Union[int, float]]:
156248
"""
157249
Serialize a Balance object to a consistent dictionary format.

bittensor_cli/src/commands/liquidity/liquidity.py

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
json_console,
1515
print_extrinsic_id,
1616
)
17+
from bittensor_cli.src.bittensor.json_utils import (
18+
print_transaction_response,
19+
print_json_data,
20+
TransactionResult,
21+
MultiTransactionResult,
22+
)
1723
from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float
1824
from bittensor_cli.src.commands.liquidity.utils import (
1925
LiquidityPosition,
@@ -291,13 +297,7 @@ async def add_liquidity(
291297
else:
292298
ext_id = None
293299
if json_output:
294-
json_console.print_json(
295-
data={
296-
"success": success,
297-
"message": message,
298-
"extrinsic_identifier": ext_id,
299-
}
300-
)
300+
print_transaction_response(success, message, ext_id)
301301
else:
302302
if success:
303303
console.print(
@@ -554,9 +554,7 @@ async def show_liquidity_list(
554554
if not json_output:
555555
console.print(liquidity_table)
556556
else:
557-
json_console.print(
558-
json.dumps({"success": True, "err_msg": "", "positions": json_table})
559-
)
557+
print_json_data({"success": True, "err_msg": "", "positions": json_table})
560558

561559

562560
async def remove_liquidity(
@@ -578,9 +576,7 @@ async def remove_liquidity(
578576
success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid)
579577
if not success:
580578
if json_output:
581-
json_console.print_json(
582-
data={"success": False, "err_msg": msg, "positions": positions}
583-
)
579+
print_json_data({"success": False, "err_msg": msg, "positions": positions})
584580
else:
585581
return err_console.print(f"Error: {msg}")
586582
return None
@@ -621,14 +617,11 @@ async def remove_liquidity(
621617
else:
622618
err_console.print(f"[red] Error removing {posid}: {msg}")
623619
else:
624-
json_table = {}
620+
json_results = MultiTransactionResult()
625621
for (success, msg, ext_receipt), posid in zip(results, position_ids):
626-
json_table[posid] = {
627-
"success": success,
628-
"err_msg": msg,
629-
"extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(),
630-
}
631-
json_console.print_json(data=json_table)
622+
ext_id = await ext_receipt.get_extrinsic_identifier() if ext_receipt else None
623+
json_results.add(str(posid), success, msg, ext_id)
624+
json_results.print()
632625
return None
633626

634627

@@ -647,7 +640,7 @@ async def modify_liquidity(
647640
if not await subtensor.subnet_exists(netuid=netuid):
648641
err_msg = f"Subnet with netuid: {netuid} does not exist in {subtensor}."
649642
if json_output:
650-
json_console.print(json.dumps({"success": False, "err_msg": err_msg}))
643+
print_transaction_response(False, err_msg, None)
651644
else:
652645
err_console.print(err_msg)
653646
return False
@@ -675,9 +668,7 @@ async def modify_liquidity(
675668
)
676669
if json_output:
677670
ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
678-
json_console.print_json(
679-
data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id}
680-
)
671+
print_transaction_response(success, msg, ext_id)
681672
else:
682673
if success:
683674
await print_extrinsic_id(ext_receipt)

bittensor_cli/src/commands/stake/add.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
get_hotkey_pub_ss58,
2727
print_extrinsic_id,
2828
)
29+
from bittensor_cli.src.bittensor.json_utils import print_json_data
2930
from bittensor_wallet import Wallet
3031

3132
if TYPE_CHECKING:
@@ -524,13 +525,11 @@ async def stake_extrinsic(
524525
staking_address
525526
] = await ext_receipt.get_extrinsic_identifier()
526527
if json_output:
527-
json_console.print_json(
528-
data={
529-
"staking_success": successes,
530-
"error_messages": error_messages,
531-
"extrinsic_ids": extrinsic_ids,
532-
}
533-
)
528+
print_json_data({
529+
"staking_success": successes,
530+
"error_messages": error_messages,
531+
"extrinsic_ids": extrinsic_ids,
532+
})
534533

535534

536535
# Helper functions

bittensor_cli/src/commands/stake/remove.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
get_hotkey_pub_ss58,
3030
print_extrinsic_id,
3131
)
32+
from bittensor_cli.src.bittensor.json_utils import print_json_data
3233

3334
if TYPE_CHECKING:
3435
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
@@ -364,7 +365,7 @@ async def unstake(
364365
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed."
365366
)
366367
if json_output:
367-
json_console.print_json(data=successes)
368+
print_json_data(successes)
368369
return True
369370

370371

@@ -570,7 +571,7 @@ async def unstake_all(
570571
"extrinsic_identifier": ext_id,
571572
}
572573
if json_output:
573-
json_console.print(json.dumps({"success": successes}))
574+
print_json_data({"success": successes})
574575

575576

576577
# Extrinsics

0 commit comments

Comments
 (0)