Skip to content

Commit 5b3383a

Browse files
committed
Fix JSON parameter parsing in warnet bitcoin rpc command
- Add _reconstruct_json_params() function to handle JSON parameters split by shell parsing - Update _rpc() to properly process JSON arrays and primitive values for bitcoin-cli - Fix issue where shell parsing would break JSON parameters into separate arguments - Handle edge cases like unquoted string arrays [network] -> [network] - Maintain backward compatibility with non-JSON parameters This fixes the issue where commands like: warnet bitcoin rpc tank-0000 getnetmsgstats '[network]' would fail due to shell parsing breaking the JSON array. Fixes #714
1 parent eb39106 commit 5b3383a

File tree

1 file changed

+136
-5
lines changed

1 file changed

+136
-5
lines changed

src/warnet/bitcoin.py

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import json
12
import os
23
import re
34
import sys
45
from datetime import datetime
56
from io import BytesIO
6-
from typing import Optional
7+
from typing import List, Optional
78

89
import click
910
from test_framework.messages import ser_uint256
@@ -25,7 +26,7 @@ def bitcoin():
2526
@click.argument("method", type=str)
2627
@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments
2728
@click.option("--namespace", default=None, show_default=True)
28-
def rpc(tank: str, method: str, params: str, namespace: Optional[str]):
29+
def rpc(tank: str, method: str, params: List[str], namespace: Optional[str]):
2930
"""
3031
Call bitcoin-cli <method> [params] on <tank pod name>
3132
"""
@@ -37,12 +38,49 @@ def rpc(tank: str, method: str, params: str, namespace: Optional[str]):
3738
print(result)
3839

3940

40-
def _rpc(tank: str, method: str, params: str, namespace: Optional[str] = None):
41+
def _rpc(tank: str, method: str, params: List[str], namespace: Optional[str] = None):
4142
# bitcoin-cli should be able to read bitcoin.conf inside the container
4243
# so no extra args like port, chain, username or password are needed
4344
namespace = get_default_namespace_or(namespace)
44-
if params:
45-
cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, params))}"
45+
46+
# Reconstruct JSON parameters that may have been split by shell parsing
47+
# This fixes issues where JSON arrays like ["network"] get split into separate arguments
48+
reconstructed_params = _reconstruct_json_params(params)
49+
50+
if reconstructed_params:
51+
# Process each parameter to handle different data types correctly for bitcoin-cli
52+
processed_params = []
53+
for param in reconstructed_params:
54+
# Handle boolean and primitive values that should not be quoted
55+
if param.lower() in ["true", "false", "null"]:
56+
processed_params.append(param.lower())
57+
elif param.isdigit() or (param.startswith("-") and param[1:].isdigit()):
58+
# Numeric values (integers, negative numbers)
59+
processed_params.append(param)
60+
else:
61+
try:
62+
# Try to parse as JSON to handle complex data structures
63+
parsed_json = json.loads(param)
64+
if isinstance(parsed_json, list):
65+
# If it's a list, extract the elements and add them individually
66+
# This ensures bitcoin-cli receives each list element as a separate argument
67+
for element in parsed_json:
68+
if isinstance(element, str):
69+
processed_params.append(f'"{element}"')
70+
else:
71+
processed_params.append(str(element))
72+
elif isinstance(parsed_json, dict):
73+
# If it's a dict, pass it as a single JSON argument
74+
# bitcoin-cli expects objects to be passed as JSON strings
75+
processed_params.append(param)
76+
else:
77+
# If it's a primitive value (number, boolean), pass it as-is
78+
processed_params.append(str(parsed_json))
79+
except json.JSONDecodeError:
80+
# Not valid JSON, pass as-is (treat as plain string)
81+
processed_params.append(param)
82+
83+
cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, processed_params))}"
4684
else:
4785
cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}"
4886
return run_command(cmd)
@@ -346,3 +384,96 @@ def to_jsonable(obj: str):
346384
return obj.hex()
347385
else:
348386
return obj
387+
388+
389+
def _reconstruct_json_params(params: List[str]) -> List[str]:
390+
"""
391+
Reconstruct JSON parameters that may have been split by shell parsing.
392+
393+
This function detects when parameters look like they should be JSON and
394+
reconstructs them properly. For example:
395+
- ['[network]'] -> ['["network"]']
396+
- ['[network,', 'message_type]'] -> ['["network", "message_type"]']
397+
- ['[{"key":', '"value"}]'] -> ['[{"key": "value"}]']
398+
399+
This fixes the issue described in GitHub issue #714 where shell parsing
400+
breaks JSON parameters into separate arguments.
401+
"""
402+
if not params:
403+
return params
404+
405+
reconstructed = []
406+
i = 0
407+
408+
while i < len(params):
409+
param = params[i]
410+
411+
# Check if this looks like the start of a JSON array or object
412+
# that was split across multiple arguments by shell parsing
413+
if (param.startswith("[") and not param.endswith("]")) or (
414+
param.startswith("{") and not param.endswith("}")
415+
):
416+
# This is the start of a JSON structure, collect all parts
417+
json_parts = [param]
418+
i += 1
419+
420+
# Collect all parts until we find the closing bracket/brace
421+
while i < len(params):
422+
next_param = params[i]
423+
json_parts.append(next_param)
424+
425+
if (param.startswith("[") and next_param.endswith("]")) or (
426+
param.startswith("{") and next_param.endswith("}")
427+
):
428+
break
429+
i += 1
430+
431+
# Reconstruct the JSON string by joining all parts
432+
json_str = " ".join(json_parts)
433+
434+
# Validate that it's valid JSON before adding
435+
try:
436+
json.loads(json_str)
437+
reconstructed.append(json_str)
438+
except json.JSONDecodeError:
439+
# If it's not valid JSON, add parts as separate parameters
440+
# This preserves the original behavior for non-JSON arguments
441+
reconstructed.extend(json_parts)
442+
443+
elif param.startswith("[") and param.endswith("]"):
444+
# Single parameter that looks like JSON array
445+
# Check if it's missing quotes around string elements
446+
if "[" in param and "]" in param and '"' not in param:
447+
# This looks like [value] without quotes, try to add them
448+
inner_content = param[1:-1] # Remove brackets
449+
if "," in inner_content:
450+
# Multiple values: [val1, val2] -> ["val1", "val2"]
451+
values = [v.strip() for v in inner_content.split(",")]
452+
quoted_values = [f'"{v}"' for v in values]
453+
reconstructed_param = f"[{', '.join(quoted_values)}]"
454+
else:
455+
# Single value: [value] -> ["value"]
456+
reconstructed_param = f'["{inner_content.strip()}"]'
457+
458+
# Validate the reconstructed JSON
459+
try:
460+
json.loads(reconstructed_param)
461+
reconstructed.append(reconstructed_param)
462+
except json.JSONDecodeError:
463+
# If reconstruction fails, keep original parameter
464+
reconstructed.append(param)
465+
else:
466+
# Already has quotes or is not a string array
467+
try:
468+
json.loads(param)
469+
reconstructed.append(param)
470+
except json.JSONDecodeError:
471+
reconstructed.append(param)
472+
473+
else:
474+
# Regular parameter, add as-is
475+
reconstructed.append(param)
476+
477+
i += 1
478+
479+
return reconstructed

0 commit comments

Comments
 (0)