#include "console_input_model.h" #include #include #include #include namespace dragonx { namespace ui { const std::vector& ConsoleRpcCommandNames() { static const std::vector commands = { "help", "getinfo", "stop", "getnetworkinfo", "getpeerinfo", "getconnectioncount", "addnode", "setban", "listbanned", "clearbanned", "ping", "getblockchaininfo", "getblockcount", "getbestblockhash", "getblock", "getblockhash", "getblockheader", "getdifficulty", "getrawmempool", "gettxout", "coinsupply", "getchaintips", "getmininginfo", "setgenerate", "getgenerate", "getnetworkhashps", "getblocksubsidy", "getbalance", "z_gettotalbalance", "z_getbalances", "getnewaddress", "z_getnewaddress", "listaddresses", "z_listaddresses", "sendtoaddress", "z_sendmany", "listtransactions", "listunspent", "z_listunspent", "z_getoperationstatus", "z_getoperationresult", "getwalletinfo", "backupwallet", "dumpprivkey", "importprivkey", "z_exportkey", "z_importkey", "signmessage", "settxfee", "createrawtransaction", "decoderawtransaction", "getrawtransaction", "sendrawtransaction", "signrawtransaction", "validateaddress", "z_validateaddress", "estimatefee", "clear" }; return commands; } void AppendConsoleHistory(std::vector& history, const std::string& command, std::size_t maxEntries) { if (command.empty()) return; if (history.empty() || history.back() != command) { history.push_back(command); while (history.size() > maxEntries) { history.erase(history.begin()); } } } int NavigateConsoleHistoryIndex(int currentIndex, std::size_t historySize, bool up) { if (historySize == 0) return -1; if (up) { if (currentIndex < 0) return static_cast(historySize) - 1; if (currentIndex > 0) return currentIndex - 1; return currentIndex; } if (currentIndex >= 0) { int next = currentIndex + 1; if (next >= static_cast(historySize)) return -1; return next; } return currentIndex; } std::string ConsoleHistoryEntry(const std::vector& history, int historyIndex) { if (historyIndex < 0 || historyIndex >= static_cast(history.size())) return {}; return history[static_cast(historyIndex)]; } ConsoleCompletionResult CompleteConsoleCommand(const std::string& input) { ConsoleCompletionResult result; if (input.empty()) return result; for (const auto& command : ConsoleRpcCommandNames()) { if (command.compare(0, input.size(), input) == 0) { result.matches.push_back(command); } } if (result.matches.empty()) return result; result.commonPrefix = result.matches.front(); for (std::size_t matchIndex = 1; matchIndex < result.matches.size(); ++matchIndex) { const auto& match = result.matches[matchIndex]; std::size_t prefixLength = 0; while (prefixLength < result.commonPrefix.size() && prefixLength < match.size() && result.commonPrefix[prefixLength] == match[prefixLength]) { ++prefixLength; } result.commonPrefix.resize(prefixLength); } return result; } std::vector FormatConsoleCompletionLines(const std::vector& matches, std::size_t maxLineLength) { std::vector lines; std::string line = " "; for (std::size_t matchIndex = 0; matchIndex < matches.size(); ++matchIndex) { if (matchIndex > 0) line += " "; line += matches[matchIndex]; if (line.length() > maxLineLength) { lines.push_back(line); line = " "; } } if (line.length() > 2) { lines.push_back(line); } return lines; } std::vector ParseConsoleCommandArgsTagged(const std::string& command) { std::vector args; std::size_t index = 0; while (index < command.size()) { while (index < command.size() && (command[index] == ' ' || command[index] == '\t')) { ++index; } if (index >= command.size()) break; ConsoleArg arg; if (command[index] == '"' || command[index] == '\'') { arg.quoted = true; char quote = command[index++]; while (index < command.size() && command[index] != quote) { arg.text += command[index++]; } if (index < command.size()) ++index; } else if (command[index] == '[' || command[index] == '{') { char open = command[index]; char close = (open == '[') ? ']' : '}'; int depth = 0; while (index < command.size()) { if (command[index] == open) ++depth; else if (command[index] == close) --depth; arg.text += command[index++]; if (depth == 0) break; } } else { while (index < command.size() && command[index] != ' ' && command[index] != '\t') { arg.text += command[index++]; } } // A quoted empty string ("") is a real argument; only skip genuinely-empty unquoted tokens. if (!arg.text.empty() || arg.quoted) args.push_back(std::move(arg)); } return args; } std::vector ParseConsoleCommandArgs(const std::string& command) { std::vector args; for (auto& a : ParseConsoleCommandArgsTagged(command)) args.push_back(std::move(a.text)); return args; } namespace { // Interpret a bare token as a JSON number only when the WHOLE token is a valid, finite number; // otherwise return it as a string. Guards against std::stoll silently truncating "1e9"/"123abc" // (it stops at the first non-digit) and std::stod yielding inf/subnormal for out-of-range input. nlohmann::json ParseConsoleNumberOrString(const std::string& s) { if (!s.empty()) { try { std::size_t pos = 0; long long ll = std::stoll(s, &pos); if (pos == s.size()) return ll; } catch (...) {} try { std::size_t pos = 0; double d = std::stod(s, &pos); if (pos == s.size() && std::isfinite(d)) return d; } catch (...) {} } return s; } } // namespace ConsoleRpcCall BuildConsoleRpcCall(const std::string& command) { auto args = ParseConsoleCommandArgsTagged(command); ConsoleRpcCall call; if (args.empty()) return call; call.valid = true; call.method = args.front().text; for (std::size_t argIndex = 1; argIndex < args.size(); ++argIndex) { const ConsoleArg& a = args[argIndex]; const std::string& arg = a.text; // A quoted token is always a string — never coerced — so a genuinely-string argument that // happens to look numeric (e.g. an all-digit label) is sent as a string, not a number. if (a.quoted) { call.params.push_back(arg); continue; } if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) { auto parsed = nlohmann::json::parse(arg, nullptr, false); if (!parsed.is_discarded()) { call.params.push_back(parsed); continue; } // Malformed JSON — fail the whole command instead of silently sending it as a string. call.valid = false; call.error = "Argument " + std::to_string(argIndex) + " is not valid JSON: " + arg; return call; } if (arg == "true") { call.params.push_back(true); } else if (arg == "false") { call.params.push_back(false); } else { call.params.push_back(ParseConsoleNumberOrString(arg)); } } return call; } std::vector FormatConsoleRpcResultLines(const std::string& result, bool isError) { if (isError) { return {{"Error: " + result, ConsoleResultLineRole::Error}}; } std::vector lines; std::string normalized = (result == "null") ? "(no result)" : result; bool isJson = !normalized.empty() && (normalized[0] == '{' || normalized[0] == '['); std::istringstream stream(normalized); std::string line; while (std::getline(stream, line)) { ConsoleResultLineRole role = ConsoleResultLineRole::Result; if (isJson && !line.empty()) { std::string trimmed = line; std::size_t first = trimmed.find_first_not_of(" \t"); if (first != std::string::npos) trimmed = trimmed.substr(first); if (!trimmed.empty()) { unsigned char firstChar = static_cast(trimmed[0]); if (trimmed[0] == '{' || trimmed[0] == '}' || trimmed[0] == '[' || trimmed[0] == ']') { role = ConsoleResultLineRole::JsonBrace; } else if (trimmed[0] == '"') { if (trimmed.find("\": ") != std::string::npos || trimmed.find("\":") != std::string::npos) { role = ConsoleResultLineRole::JsonKey; } else { role = ConsoleResultLineRole::JsonString; } } else if (std::isdigit(firstChar) || trimmed[0] == '-') { role = ConsoleResultLineRole::JsonNumber; } else if (trimmed == "true," || trimmed == "false," || trimmed == "true" || trimmed == "false" || trimmed == "null," || trimmed == "null") { role = ConsoleResultLineRole::JsonNumber; } } } lines.push_back({line, role}); } return lines; } std::vector ComputeConsoleFoldSpans(const std::vector& lines) { std::vector spans(lines.size(), 0); auto leadingSpaces = [](const std::string& s) -> std::size_t { std::size_t n = 0; while (n < s.size() && s[n] == ' ') ++n; return n; }; auto lastNonSpace = [](const std::string& s) -> char { for (std::size_t i = s.size(); i-- > 0; ) if (s[i] != ' ' && s[i] != '\t') return s[i]; return '\0'; }; auto firstNonSpace = [](const std::string& s) -> char { for (char c : s) if (c != ' ' && c != '\t') return c; return '\0'; }; for (std::size_t i = 0; i < lines.size(); ++i) { const char last = lastNonSpace(lines[i]); if (last != '{' && last != '[') continue; // not a block opener const std::size_t indent = leadingSpaces(lines[i]); const char wantClose = (last == '{') ? '}' : ']'; // Matching closer = the next line at the same indentation whose first non-space char // is the corresponding closing bracket (pretty-printed JSON has aligned brackets). for (std::size_t j = i + 1; j < lines.size(); ++j) { if (leadingSpaces(lines[j]) == indent && firstNonSpace(lines[j]) == wantClose) { if (j - i >= 2) spans[i] = static_cast(j - i); // >=1 inner line break; } } } return spans; } } // namespace ui } // namespace dragonx