Skip to content

Commit 0cbe07e

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 0cbe07e

File tree

7 files changed

+251
-135
lines changed

7 files changed

+251
-135
lines changed

bittensor_cli/src/bittensor/json_utils.py

Lines changed: 125 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,138 @@
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
2626
json_console = Console()
2727

2828

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

63166
def json_success(data: Any) -> str:
64167
"""
65-
Create a successful JSON response.
168+
Create a successful JSON response string.
66169
67170
Args:
68171
data: Response data to include
@@ -75,7 +178,7 @@ def json_success(data: Any) -> str:
75178

76179
def json_error(error: str, data: Optional[Any] = None) -> str:
77180
"""
78-
Create an error JSON response.
181+
Create an error JSON response string.
79182
80183
Args:
81184
error: Error message describing what went wrong
@@ -87,43 +190,9 @@ def json_error(error: str, data: Optional[Any] = None) -> str:
87190
return json_response(success=False, data=data, error=error)
88191

89192

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-
124193
def print_json(response: str) -> None:
125194
"""
126-
Print a JSON response to the console.
195+
Print a JSON string response to the console.
127196
128197
Args:
129198
response: JSON string to print
@@ -152,6 +221,16 @@ def print_json_error(error: str, data: Optional[Any] = None) -> None:
152221
print_json(json_error(error, data))
153222

154223

224+
def print_json_data(data: Any) -> None:
225+
"""
226+
Print data directly as JSON (for simple data responses).
227+
228+
Args:
229+
data: Data to print as JSON
230+
"""
231+
json_console.print_json(data=data)
232+
233+
155234
def serialize_balance(balance: Any) -> dict[str, Union[int, float]]:
156235
"""
157236
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)