From 86c22e6250e2830825c4f6de3d6508db6e2a3443 Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Mon, 23 Jun 2025 18:45:59 +0300 Subject: [PATCH 1/6] 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 --- src/warnet/bitcoin.py | 139 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 4 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 9d0c54f50..63ab8cc75 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,3 +1,4 @@ +import json import os import re import sys @@ -25,7 +26,7 @@ def bitcoin(): @click.argument("method", type=str) @click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments @click.option("--namespace", default=None, show_default=True) -def rpc(tank: str, method: str, params: str, namespace: Optional[str]): +def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): """ Call bitcoin-cli [params] on """ @@ -37,12 +38,49 @@ def rpc(tank: str, method: str, params: str, namespace: Optional[str]): print(result) -def _rpc(tank: str, method: str, params: str, namespace: Optional[str] = None): +def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): # bitcoin-cli should be able to read bitcoin.conf inside the container # so no extra args like port, chain, username or password are needed namespace = get_default_namespace_or(namespace) - if params: - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, params))}" + + # Reconstruct JSON parameters that may have been split by shell parsing + # This fixes issues where JSON arrays like ["network"] get split into separate arguments + reconstructed_params = _reconstruct_json_params(params) + + if reconstructed_params: + # Process each parameter to handle different data types correctly for bitcoin-cli + processed_params = [] + for param in reconstructed_params: + # Handle boolean and primitive values that should not be quoted + if param.lower() in ["true", "false", "null"]: + processed_params.append(param.lower()) + elif param.isdigit() or (param.startswith("-") and param[1:].isdigit()): + # Numeric values (integers, negative numbers) + processed_params.append(param) + else: + try: + # Try to parse as JSON to handle complex data structures + parsed_json = json.loads(param) + if isinstance(parsed_json, list): + # If it's a list, extract the elements and add them individually + # This ensures bitcoin-cli receives each list element as a separate argument + for element in parsed_json: + if isinstance(element, str): + processed_params.append(f'"{element}"') + else: + processed_params.append(str(element)) + elif isinstance(parsed_json, dict): + # If it's a dict, pass it as a single JSON argument + # bitcoin-cli expects objects to be passed as JSON strings + processed_params.append(param) + else: + # If it's a primitive value (number, boolean), pass it as-is + processed_params.append(str(parsed_json)) + except json.JSONDecodeError: + # Not valid JSON, pass as-is (treat as plain string) + processed_params.append(param) + + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, processed_params))}" else: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" return run_command(cmd) @@ -346,3 +384,96 @@ def to_jsonable(obj: str): return obj.hex() else: return obj + + +def _reconstruct_json_params(params: list[str]) -> list[str]: + """ + Reconstruct JSON parameters that may have been split by shell parsing. + + This function detects when parameters look like they should be JSON and + reconstructs them properly. For example: + - ['[network]'] -> ['["network"]'] + - ['[network,', 'message_type]'] -> ['["network", "message_type"]'] + - ['[{"key":', '"value"}]'] -> ['[{"key": "value"}]'] + + This fixes the issue described in GitHub issue #714 where shell parsing + breaks JSON parameters into separate arguments. + """ + if not params: + return params + + reconstructed = [] + i = 0 + + while i < len(params): + param = params[i] + + # Check if this looks like the start of a JSON array or object + # that was split across multiple arguments by shell parsing + if (param.startswith("[") and not param.endswith("]")) or ( + param.startswith("{") and not param.endswith("}") + ): + # This is the start of a JSON structure, collect all parts + json_parts = [param] + i += 1 + + # Collect all parts until we find the closing bracket/brace + while i < len(params): + next_param = params[i] + json_parts.append(next_param) + + if (param.startswith("[") and next_param.endswith("]")) or ( + param.startswith("{") and next_param.endswith("}") + ): + break + i += 1 + + # Reconstruct the JSON string by joining all parts + json_str = " ".join(json_parts) + + # Validate that it's valid JSON before adding + try: + json.loads(json_str) + reconstructed.append(json_str) + except json.JSONDecodeError: + # If it's not valid JSON, add parts as separate parameters + # This preserves the original behavior for non-JSON arguments + reconstructed.extend(json_parts) + + elif param.startswith("[") and param.endswith("]"): + # Single parameter that looks like JSON array + # Check if it's missing quotes around string elements + if "[" in param and "]" in param and '"' not in param: + # This looks like [value] without quotes, try to add them + inner_content = param[1:-1] # Remove brackets + if "," in inner_content: + # Multiple values: [val1, val2] -> ["val1", "val2"] + values = [v.strip() for v in inner_content.split(",")] + quoted_values = [f'"{v}"' for v in values] + reconstructed_param = f"[{', '.join(quoted_values)}]" + else: + # Single value: [value] -> ["value"] + reconstructed_param = f'["{inner_content.strip()}"]' + + # Validate the reconstructed JSON + try: + json.loads(reconstructed_param) + reconstructed.append(reconstructed_param) + except json.JSONDecodeError: + # If reconstruction fails, keep original parameter + reconstructed.append(param) + else: + # Already has quotes or is not a string array + try: + json.loads(param) + reconstructed.append(param) + except json.JSONDecodeError: + reconstructed.append(param) + + else: + # Regular parameter, add as-is + reconstructed.append(param) + + i += 1 + + return reconstructed From 84493379b2d578f376dddc0edbcbc06a4178fa0f Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Thu, 3 Jul 2025 02:34:24 +0300 Subject: [PATCH 2/6] fix: robust JSON parameter handling for warnet bitcoin rpc - Accepts JSON params with or without backslash-escaped quotes (e.g. '[http]' and '["http"]') - Wraps JSON params in single quotes for correct shell and bitcoin-cli parsing --- src/warnet/bitcoin.py | 143 +++++------------------------------------- 1 file changed, 14 insertions(+), 129 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 63ab8cc75..b096fa4b1 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -43,44 +43,22 @@ def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = N # so no extra args like port, chain, username or password are needed namespace = get_default_namespace_or(namespace) - # Reconstruct JSON parameters that may have been split by shell parsing - # This fixes issues where JSON arrays like ["network"] get split into separate arguments - reconstructed_params = _reconstruct_json_params(params) - - if reconstructed_params: - # Process each parameter to handle different data types correctly for bitcoin-cli + if params: + # Process parameters to ensure proper shell escaping processed_params = [] - for param in reconstructed_params: - # Handle boolean and primitive values that should not be quoted - if param.lower() in ["true", "false", "null"]: - processed_params.append(param.lower()) - elif param.isdigit() or (param.startswith("-") and param[1:].isdigit()): - # Numeric values (integers, negative numbers) - processed_params.append(param) + for param in params: + # If the parameter looks like JSON (starts with [ or {), fix malformed patterns + if param.startswith("[") or param.startswith("{"): + # Fix common malformed JSON patterns + if '\\"' in param: + # Convert [\"value\"] to ["value"] + param = param.replace('\\"', '"') + # Wrap JSON in single quotes to preserve it as a single argument + processed_params.append(f"'{param}'") else: - try: - # Try to parse as JSON to handle complex data structures - parsed_json = json.loads(param) - if isinstance(parsed_json, list): - # If it's a list, extract the elements and add them individually - # This ensures bitcoin-cli receives each list element as a separate argument - for element in parsed_json: - if isinstance(element, str): - processed_params.append(f'"{element}"') - else: - processed_params.append(str(element)) - elif isinstance(parsed_json, dict): - # If it's a dict, pass it as a single JSON argument - # bitcoin-cli expects objects to be passed as JSON strings - processed_params.append(param) - else: - # If it's a primitive value (number, boolean), pass it as-is - processed_params.append(str(parsed_json)) - except json.JSONDecodeError: - # Not valid JSON, pass as-is (treat as plain string) - processed_params.append(param) - - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, processed_params))}" + processed_params.append(param) + + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(processed_params)}" else: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" return run_command(cmd) @@ -384,96 +362,3 @@ def to_jsonable(obj: str): return obj.hex() else: return obj - - -def _reconstruct_json_params(params: list[str]) -> list[str]: - """ - Reconstruct JSON parameters that may have been split by shell parsing. - - This function detects when parameters look like they should be JSON and - reconstructs them properly. For example: - - ['[network]'] -> ['["network"]'] - - ['[network,', 'message_type]'] -> ['["network", "message_type"]'] - - ['[{"key":', '"value"}]'] -> ['[{"key": "value"}]'] - - This fixes the issue described in GitHub issue #714 where shell parsing - breaks JSON parameters into separate arguments. - """ - if not params: - return params - - reconstructed = [] - i = 0 - - while i < len(params): - param = params[i] - - # Check if this looks like the start of a JSON array or object - # that was split across multiple arguments by shell parsing - if (param.startswith("[") and not param.endswith("]")) or ( - param.startswith("{") and not param.endswith("}") - ): - # This is the start of a JSON structure, collect all parts - json_parts = [param] - i += 1 - - # Collect all parts until we find the closing bracket/brace - while i < len(params): - next_param = params[i] - json_parts.append(next_param) - - if (param.startswith("[") and next_param.endswith("]")) or ( - param.startswith("{") and next_param.endswith("}") - ): - break - i += 1 - - # Reconstruct the JSON string by joining all parts - json_str = " ".join(json_parts) - - # Validate that it's valid JSON before adding - try: - json.loads(json_str) - reconstructed.append(json_str) - except json.JSONDecodeError: - # If it's not valid JSON, add parts as separate parameters - # This preserves the original behavior for non-JSON arguments - reconstructed.extend(json_parts) - - elif param.startswith("[") and param.endswith("]"): - # Single parameter that looks like JSON array - # Check if it's missing quotes around string elements - if "[" in param and "]" in param and '"' not in param: - # This looks like [value] without quotes, try to add them - inner_content = param[1:-1] # Remove brackets - if "," in inner_content: - # Multiple values: [val1, val2] -> ["val1", "val2"] - values = [v.strip() for v in inner_content.split(",")] - quoted_values = [f'"{v}"' for v in values] - reconstructed_param = f"[{', '.join(quoted_values)}]" - else: - # Single value: [value] -> ["value"] - reconstructed_param = f'["{inner_content.strip()}"]' - - # Validate the reconstructed JSON - try: - json.loads(reconstructed_param) - reconstructed.append(reconstructed_param) - except json.JSONDecodeError: - # If reconstruction fails, keep original parameter - reconstructed.append(param) - else: - # Already has quotes or is not a string array - try: - json.loads(param) - reconstructed.append(param) - except json.JSONDecodeError: - reconstructed.append(param) - - else: - # Regular parameter, add as-is - reconstructed.append(param) - - i += 1 - - return reconstructed From 881dfabbf3c4f45410d3ee7c01e12c93bf2f9908 Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Thu, 3 Jul 2025 02:58:13 +0300 Subject: [PATCH 3/6] fix: handle JSON array parameters intelligently for different RPC methods - Fix getblock '[blockhash]' issue by extracting first element from JSON arrays - Maintain backward compatibility for plain string parameters - Support JSON arrays as-is for methods like logging and importdescriptors - Extract only first element for single-parameter methods (getblock, getblockhash, gettransaction) - Extract all elements for multi-parameter methods - Fix malformed JSON patterns with escaped quotes --- src/warnet/bitcoin.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index b096fa4b1..76a2e5b77 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -53,8 +53,34 @@ def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = N if '\\"' in param: # Convert [\"value\"] to ["value"] param = param.replace('\\"', '"') - # Wrap JSON in single quotes to preserve it as a single argument - processed_params.append(f"'{param}'") + + # Try to parse as JSON to determine how to handle it + try: + parsed_json = json.loads(param) + if isinstance(parsed_json, list): + # For JSON arrays, extract elements for methods that expect individual parameters + # (like getblock, gettransaction, etc.) + # But keep as JSON for methods that expect JSON arrays (like logging) + if method in ["logging", "importdescriptors", "importmulti"]: + # These methods expect JSON arrays as-is + processed_params.append(f"'{param}'") + else: + # For single-parameter methods, only extract the first element + # For multi-parameter methods, extract all elements + if method in ["getblockhash", "getblock", "gettransaction"]: + # These methods expect a single parameter, so only take the first element + if parsed_json: + processed_params.append(str(parsed_json[0])) + else: + # Extract all array elements for other methods + for element in parsed_json: + processed_params.append(str(element)) + else: + # For JSON objects, pass as-is + processed_params.append(f"'{param}'") + except json.JSONDecodeError: + # If it's not valid JSON, pass as-is + processed_params.append(param) else: processed_params.append(param) From 7dbbe600bfc87975a44ee96df1482fbb21080948 Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Tue, 8 Jul 2025 12:21:59 +0300 Subject: [PATCH 4/6] Simplify bitcoin RPC parameter handling with pure passthrough approach --- src/warnet/bitcoin.py | 49 +++++-------------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 76a2e5b77..abd4a3251 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,6 +1,6 @@ -import json import os import re +import shlex import sys from datetime import datetime from io import BytesIO @@ -39,54 +39,15 @@ def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): - # bitcoin-cli should be able to read bitcoin.conf inside the container - # so no extra args like port, chain, username or password are needed namespace = get_default_namespace_or(namespace) if params: - # Process parameters to ensure proper shell escaping - processed_params = [] - for param in params: - # If the parameter looks like JSON (starts with [ or {), fix malformed patterns - if param.startswith("[") or param.startswith("{"): - # Fix common malformed JSON patterns - if '\\"' in param: - # Convert [\"value\"] to ["value"] - param = param.replace('\\"', '"') - - # Try to parse as JSON to determine how to handle it - try: - parsed_json = json.loads(param) - if isinstance(parsed_json, list): - # For JSON arrays, extract elements for methods that expect individual parameters - # (like getblock, gettransaction, etc.) - # But keep as JSON for methods that expect JSON arrays (like logging) - if method in ["logging", "importdescriptors", "importmulti"]: - # These methods expect JSON arrays as-is - processed_params.append(f"'{param}'") - else: - # For single-parameter methods, only extract the first element - # For multi-parameter methods, extract all elements - if method in ["getblockhash", "getblock", "gettransaction"]: - # These methods expect a single parameter, so only take the first element - if parsed_json: - processed_params.append(str(parsed_json[0])) - else: - # Extract all array elements for other methods - for element in parsed_json: - processed_params.append(str(element)) - else: - # For JSON objects, pass as-is - processed_params.append(f"'{param}'") - except json.JSONDecodeError: - # If it's not valid JSON, pass as-is - processed_params.append(param) - else: - processed_params.append(param) - - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(processed_params)}" + # Shell-escape each param to preserve quotes and special characters + bitcoin_cli_args = " ".join(shlex.quote(p) for p in params) + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {bitcoin_cli_args}" else: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" + return run_command(cmd) From b76cb94c3fd8b3a11380f2db64023360e47aba3b Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Wed, 9 Jul 2025 00:39:19 +0300 Subject: [PATCH 5/6] fix: resolve JSON parsing errors in bitcoin rpc command Use click.UNPROCESSED to prevent JSON splitting, enabling proper descriptor import and fixing signet test timeout. --- src/warnet/bitcoin.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index abd4a3251..2fdf12ee3 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -24,7 +24,7 @@ def bitcoin(): @bitcoin.command(context_settings={"ignore_unknown_options": True}) @click.argument("tank", type=str) @click.argument("method", type=str) -@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments +@click.argument("params", type=click.UNPROCESSED, nargs=-1) # get raw unprocessed arguments @click.option("--namespace", default=None, show_default=True) def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): """ @@ -39,12 +39,28 @@ def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): + # bitcoin-cli should be able to read bitcoin.conf inside the container + # so no extra args like port, chain, username or password are needed namespace = get_default_namespace_or(namespace) if params: - # Shell-escape each param to preserve quotes and special characters - bitcoin_cli_args = " ".join(shlex.quote(p) for p in params) - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {bitcoin_cli_args}" + # Check if this looks like a JSON argument (starts with [ or {) + param_str = " ".join(params) + if param_str.strip().startswith("[") or param_str.strip().startswith("{"): + # For JSON arguments, ensure it's passed as a single argument + # Remove any extra quotes that might have been added by the shell + param_str = param_str.strip() + if ( + param_str.startswith("'") + and param_str.endswith("'") + or param_str.startswith('"') + and param_str.endswith('"') + ): + param_str = param_str[1:-1] + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {shlex.quote(param_str)}" + else: + # For non-JSON arguments, use simple space joining + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {param_str}" else: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" From 54606ea4fdf021270e6a8c65e6eed7fd5eaa4458 Mon Sep 17 00:00:00 2001 From: b-l-u-e Date: Thu, 17 Jul 2025 00:24:10 +0300 Subject: [PATCH 6/6] fix: improve bitcoin RPC argument handling for JSON and mixed arguments --- src/warnet/bitcoin.py | 44 ++++++----- test/signet_test.py | 2 +- test/test_bitcoin_rpc_args.py | 144 ++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 test/test_bitcoin_rpc_args.py diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 2fdf12ee3..5b392e22e 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,3 +1,4 @@ +import json import os import re import shlex @@ -39,29 +40,34 @@ def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): - # bitcoin-cli should be able to read bitcoin.conf inside the container - # so no extra args like port, chain, username or password are needed namespace = get_default_namespace_or(namespace) if params: - # Check if this looks like a JSON argument (starts with [ or {) - param_str = " ".join(params) - if param_str.strip().startswith("[") or param_str.strip().startswith("{"): - # For JSON arguments, ensure it's passed as a single argument - # Remove any extra quotes that might have been added by the shell - param_str = param_str.strip() - if ( - param_str.startswith("'") - and param_str.endswith("'") - or param_str.startswith('"') - and param_str.endswith('"') - ): - param_str = param_str[1:-1] - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {shlex.quote(param_str)}" - else: - # For non-JSON arguments, use simple space joining - cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {param_str}" + # First, try to join all parameters into a single string. + full_param_str = " ".join(params) + + try: + # Heuristic: if the string looks like a JSON object/array, try to parse it. + # This handles the `signet_test` case where one large JSON argument was split + # by the shell into multiple params. + if full_param_str.strip().startswith(("[", "{")): + json.loads(full_param_str) + # SUCCESS: The params form a single, valid JSON object. + # Quote the entire reconstructed string as one argument. + param_str = shlex.quote(full_param_str) + else: + # It's not a JSON object, so it must be multiple distinct arguments. + # Raise an error to fall through to the individual quoting logic. + raise ValueError + except (json.JSONDecodeError, ValueError): + # FAILURE: The params are not one single JSON object. + # This handles the `rpc_test` case with mixed arguments. + # Quote each parameter individually to preserve them as separate arguments. + param_str = " ".join(shlex.quote(p) for p in params) + + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {param_str}" else: + # Handle commands with no parameters cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" return run_command(cmd) diff --git a/test/signet_test.py b/test/signet_test.py index 93fa2e484..8f16b9a5c 100755 --- a/test/signet_test.py +++ b/test/signet_test.py @@ -34,7 +34,7 @@ def setup_network(self): def check_signet_miner(self): self.warnet("bitcoin rpc miner createwallet miner") self.warnet( - f"bitcoin rpc miner importdescriptors '{json.dumps(self.signer_data['descriptors'])}'" + f"bitcoin rpc miner importdescriptors {json.dumps(self.signer_data['descriptors'])}" ) self.warnet( f"run resources/scenarios/signet_miner.py --tank=0 generate --max-blocks=8 --min-nbits --address={self.signer_data['address']['address']}" diff --git a/test/test_bitcoin_rpc_args.py b/test/test_bitcoin_rpc_args.py new file mode 100644 index 000000000..6a40a694a --- /dev/null +++ b/test/test_bitcoin_rpc_args.py @@ -0,0 +1,144 @@ +import shlex +import sys +from pathlib import Path +from unittest.mock import patch + +# Import TestBase for consistent test structure +from test_base import TestBase + +# Import _rpc from warnet.bitcoin and run_command from warnet.process +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) +from warnet.bitcoin import _rpc + +# Edge cases to test +EDGE_CASES = [ + # (params, expected_cmd_suffix, should_fail) + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + False, + ), + # Malformed JSON (should fail gracefully) + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}' + ], # Missing closing bracket + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}', + ], + True, # Should fail due to malformed JSON + ), + # Unicode in descriptors + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}' + ], + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}', + ], + False, + ), + # Long descriptor (simulate, should not crash, may fail) + ( + [ + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]" + ], + [ + "send", + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]", + ], + False, # Updated to False since it now works correctly + ), + # Empty params + ([], ["send"], False), +] + + +class BitcoinRPCRPCArgsTest(TestBase): + def __init__(self): + super().__init__() + self.tank = "tank-0027" + self.namespace = "default" + self.captured_cmds = [] + + def run_test(self): + self.log.info("Testing bitcoin _rpc argument handling edge cases") + for params, expected_suffix, should_fail in EDGE_CASES: + # Extract the method from the expected suffix + method = expected_suffix[0] + + with patch("warnet.bitcoin.run_command") as mock_run_command: + mock_run_command.return_value = "MOCKED" + try: + _rpc(self.tank, method, params, self.namespace) + called_args = mock_run_command.call_args[0][0] + self.captured_cmds.append(called_args) + # Parse the command string into arguments for comparison + parsed_args = shlex.split(called_args) + assert parsed_args[-len(expected_suffix) :] == expected_suffix, ( + f"Params: {params} | Got: {parsed_args[-len(expected_suffix) :]} | Expected: {expected_suffix}" + ) + if should_fail: + self.log.info(f"Expected failure for params: {params}, but succeeded.") + except Exception as e: + if not should_fail: + raise AssertionError(f"Unexpected failure for params: {params}: {e}") from e + self.log.info(f"Expected failure for params: {params}: {e}") + self.log.info("All edge case argument tests passed.") + + +if __name__ == "__main__": + test = BitcoinRPCRPCArgsTest() + test.run_test()