limenka-cli.cpp raw
1 // Copyright (c) 2009-2010 Satoshi Nakamoto
2 // Copyright (c) 2009-2022 The Limenka developers
3 // Distributed under the MIT software license, see the accompanying
4 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6 #include <limenka-build-config.h> // IWYU pragma: keep
7
8 #include <chainparamsbase.h>
9 #include <clientversion.h>
10 #include <common/args.h>
11 #include <common/system.h>
12 #include <compat/compat.h>
13 #include <compat/stdin.h>
14 #include <consensus/amount.h>
15 #include <policy/feerate.h>
16 #include <rpc/client.h>
17 #include <rpc/mining.h>
18 #include <rpc/protocol.h>
19 #include <rpc/request.h>
20 #include <tinyformat.h>
21 #include <univalue.h>
22 #include <util/chaintype.h>
23 #include <util/exception.h>
24 #include <util/strencodings.h>
25 #include <util/time.h>
26 #include <util/translation.h>
27
28 #include <algorithm>
29 #include <chrono>
30 #include <cmath>
31 #include <cstdio>
32 #include <functional>
33 #include <memory>
34 #include <optional>
35 #include <string>
36 #include <tuple>
37
38 #ifndef WIN32
39 #include <unistd.h>
40 #endif
41
42 #include <event2/buffer.h>
43 #include <event2/keyvalq_struct.h>
44 #include <support/events.h>
45
46 using util::Join;
47 using util::ToString;
48
49 // The server returns time values from a mockable system clock, but it is not
50 // trivial to get the mocked time from the server, nor is it needed for now, so
51 // just use a plain system_clock.
52 using CliClock = std::chrono::system_clock;
53
54 const TranslateFn G_TRANSLATION_FUN{nullptr};
55
56 static const char DEFAULT_RPCCONNECT[] = "127.0.0.1";
57 static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900;
58 static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
59 static const bool DEFAULT_NAMED=false;
60 static const int CONTINUE_EXECUTION=-1;
61 static constexpr uint8_t NETINFO_MAX_LEVEL{4};
62 static constexpr int8_t UNKNOWN_NETWORK{-1};
63 // See GetNetworkName() in netbase.cpp
64 static constexpr std::array NETWORKS{"not_publicly_routable", "ipv4", "ipv6", "onion", "i2p", "cjdns", "internal"};
65 static constexpr std::array NETWORK_SHORT_NAMES{"npr", "ipv4", "ipv6", "onion", "i2p", "cjdns", "int"};
66 static constexpr std::array UNREACHABLE_NETWORK_IDS{/*not_publicly_routable*/0, /*internal*/6};
67
68 /** Default number of blocks to generate for RPC generatetoaddress. */
69 static const std::string DEFAULT_NBLOCKS = "1";
70
71 /** Default -color setting. */
72 static const std::string DEFAULT_COLOR_SETTING{"auto"};
73
74 static void SetupCliArgs(ArgsManager& argsman)
75 {
76 SetupHelpOptions(argsman);
77
78 const auto defaultBaseParams = CreateBaseChainParams(ChainType::MAIN);
79 const auto testnetBaseParams = CreateBaseChainParams(ChainType::TESTNET);
80 const auto testnet4BaseParams = CreateBaseChainParams(ChainType::TESTNET4);
81 const auto signetBaseParams = CreateBaseChainParams(ChainType::SIGNET);
82 const auto regtestBaseParams = CreateBaseChainParams(ChainType::REGTEST);
83
84 argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
85 argsman.AddArg("-conf=<file>", strprintf("Specify configuration file. Relative paths will be prefixed by datadir location. (default: %s)", LIMENKA_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
86 argsman.AddArg("-confrw=<file>", strprintf("Specify read/write configuration file. Relative paths will be prefixed by the network-specific datadir location. (default: %s)", LIMENKA_RW_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
87 argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
88 argsman.AddArg("-generate",
89 strprintf("Generate blocks, equivalent to RPC getnewaddress followed by RPC generatetoaddress. Optional positional integer "
90 "arguments are number of blocks to generate (default: %s) and maximum iterations to try (default: %s), equivalent to "
91 "RPC generatetoaddress nblocks and maxtries arguments. Example: limenka-cli -generate 4 1000",
92 DEFAULT_NBLOCKS, DEFAULT_MAX_TRIES),
93 ArgsManager::ALLOW_ANY, OptionsCategory::CLI_COMMANDS);
94 argsman.AddArg("-addrinfo", "Get the number of addresses known to the node, per network and total, after filtering for quality and recency. The total number of addresses known to the node may be higher.", ArgsManager::ALLOW_ANY, OptionsCategory::CLI_COMMANDS);
95 argsman.AddArg("-getinfo", "Get general information from the remote server, including the total balance and the balances of each loaded wallet when in multiwallet mode. Note that -getinfo is the combined result of several RPCs (getnetworkinfo, getblockchaininfo, getwalletinfo, getbalances, and in multiwallet mode, listwallets), each with potentially different state.", ArgsManager::ALLOW_ANY, OptionsCategory::CLI_COMMANDS);
96 argsman.AddArg("-netinfo", strprintf("Get network peer connection information from the remote server. An optional argument from 0 to %d can be passed for different peers listings (default: 0). If a non-zero value is passed, an additional \"outonly\" (or \"o\") argument can be passed to see outbound peers only. Pass \"help\" (or \"h\") for detailed help documentation.", NETINFO_MAX_LEVEL), ArgsManager::ALLOW_ANY, OptionsCategory::CLI_COMMANDS);
97
98 SetupChainParamsBaseOptions(argsman);
99 argsman.AddArg("-color=<when>", strprintf("Color setting for CLI output (default: %s). Valid values: always, auto (add color codes when standard output is connected to a terminal and OS is not WIN32), never. Only applies to the output of -getinfo.", DEFAULT_COLOR_SETTING), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
100 argsman.AddArg("-named", strprintf("Pass named instead of positional arguments (default: %s)", DEFAULT_NAMED), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
101 argsman.AddArg("-rpcclienttimeout=<n>", strprintf("Timeout in seconds during HTTP requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
102 argsman.AddArg("-rpcconnect=<ip>", strprintf("Send commands to node running on <ip> (default: %s)", DEFAULT_RPCCONNECT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
103 argsman.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
104 argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
105 argsman.AddArg("-rpcport=<port>", strprintf("Connect to JSON-RPC on <port> (default: %u, testnet: %u, testnet4: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), testnet4BaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::OPTIONS);
106 argsman.AddArg("-rpcuser=<user>", "Username for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
107 argsman.AddArg("-rpcwait", "Wait for RPC server to start", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
108 argsman.AddArg("-rpcwaittimeout=<n>", strprintf("Timeout in seconds to wait for the RPC server to start, or 0 for no timeout. (default: %d)", DEFAULT_WAIT_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
109 argsman.AddArg("-rpcwallet=<walletname>", "Send RPC for non-default wallet on RPC server (needs to exactly match corresponding -wallet option passed to limenkad). This changes the RPC endpoint used, e.g. http://127.0.0.1:8332/wallet/<walletname>", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
110 argsman.AddArg("-stdin", "Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). When combined with -stdinrpcpass, the first line from standard input is used for the RPC password.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
111 argsman.AddArg("-stdinrpcpass", "Read RPC password from standard input as a single line. When combined with -stdin, the first line from standard input is used for the RPC password. When combined with -stdinwalletpassphrase, -stdinrpcpass consumes the first line, and -stdinwalletpassphrase consumes the second.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
112 argsman.AddArg("-stdinwalletpassphrase", "Read wallet passphrase from standard input as a single line. When combined with -stdin, the first line from standard input is used for the wallet passphrase.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
113 }
114
115 std::optional<std::string> RpcWalletName(const ArgsManager& args)
116 {
117 // Check IsArgNegated to return nullopt instead of "0" if -norpcwallet is specified
118 if (args.IsArgNegated("-rpcwallet")) return std::nullopt;
119 return args.GetArg("-rpcwallet");
120 }
121
122 /** libevent event log callback */
123 static void libevent_log_cb(int severity, const char *msg)
124 {
125 // Ignore everything other than errors
126 if (severity >= EVENT_LOG_ERR) {
127 throw std::runtime_error(strprintf("libevent error: %s", msg));
128 }
129 }
130
131 //
132 // Exception thrown on connection error. This error is used to determine
133 // when to wait if -rpcwait is given.
134 //
135 class CConnectionFailed : public std::runtime_error
136 {
137 public:
138
139 explicit inline CConnectionFailed(const std::string& msg) :
140 std::runtime_error(msg)
141 {}
142
143 };
144
145 //
146 // This function returns either one of EXIT_ codes when it's expected to stop the process or
147 // CONTINUE_EXECUTION when it's expected to continue further.
148 //
149 static int AppInitRPC(int argc, char* argv[])
150 {
151 SetupCliArgs(gArgs);
152 std::string error;
153 if (!gArgs.ParseParameters(argc, argv, error)) {
154 tfm::format(std::cerr, "Error parsing command line arguments: %s\n", error);
155 return EXIT_FAILURE;
156 }
157 if (argc < 2 || HelpRequested(gArgs) || gArgs.GetBoolArg("-version", false)) {
158 std::string strUsage = CLIENT_NAME " RPC client version " + FormatFullVersion() + "\n";
159
160 if (gArgs.GetBoolArg("-version", false)) {
161 strUsage += FormatParagraph(LicenseInfo());
162 } else {
163 strUsage += "\n"
164 "The limenka-cli utility provides a command line interface to interact with a " CLIENT_NAME " RPC server.\n"
165 "\nIt can be used to query network information, manage wallets, create or broadcast transactions, and control the " CLIENT_NAME " server.\n"
166 "\nUse the \"help\" command to list all commands. Use \"help <command>\" to show help for that command.\n"
167 "The -named option allows you to specify parameters using the key=value format, eliminating the need to pass unused positional parameters.\n"
168 "\n"
169 "Usage: limenka-cli [options] <command> [params]\n"
170 "or: limenka-cli [options] -named <command> [name=value]...\n"
171 "or: limenka-cli [options] help\n"
172 "or: limenka-cli [options] help <command>\n"
173 "\n";
174 strUsage += "\n" + gArgs.GetHelpMessage();
175 }
176
177 tfm::format(std::cout, "%s", strUsage);
178 if (argc < 2) {
179 tfm::format(std::cerr, "Error: too few parameters\n");
180 return EXIT_FAILURE;
181 }
182 return EXIT_SUCCESS;
183 }
184 if (!CheckDataDirOption(gArgs)) {
185 tfm::format(std::cerr, "Error: Specified data directory \"%s\" does not exist.\n", gArgs.GetArg("-datadir", ""));
186 return EXIT_FAILURE;
187 }
188 if (!gArgs.ReadConfigFiles(error, true)) {
189 tfm::format(std::cerr, "Error reading configuration file: %s\n", error);
190 return EXIT_FAILURE;
191 }
192 return CONTINUE_EXECUTION;
193 }
194
195
196 /** Reply structure for request_done to fill in */
197 struct HTTPReply
198 {
199 HTTPReply() = default;
200
201 int status{0};
202 int error{-1};
203 std::string body;
204 };
205
206 static std::string http_errorstring(int code)
207 {
208 switch(code) {
209 case EVREQ_HTTP_TIMEOUT:
210 return "timeout reached";
211 case EVREQ_HTTP_EOF:
212 return "EOF reached";
213 case EVREQ_HTTP_INVALID_HEADER:
214 return "error while reading header, or invalid header";
215 case EVREQ_HTTP_BUFFER_ERROR:
216 return "error encountered while reading or writing";
217 case EVREQ_HTTP_REQUEST_CANCEL:
218 return "request was canceled";
219 case EVREQ_HTTP_DATA_TOO_LONG:
220 return "response body is larger than allowed";
221 default:
222 return "unknown";
223 }
224 }
225
226 static void http_request_done(struct evhttp_request *req, void *ctx)
227 {
228 HTTPReply *reply = static_cast<HTTPReply*>(ctx);
229
230 if (req == nullptr) {
231 /* If req is nullptr, it means an error occurred while connecting: the
232 * error code will have been passed to http_error_cb.
233 */
234 reply->status = 0;
235 return;
236 }
237
238 reply->status = evhttp_request_get_response_code(req);
239
240 struct evbuffer *buf = evhttp_request_get_input_buffer(req);
241 if (buf)
242 {
243 size_t size = evbuffer_get_length(buf);
244 const char *data = (const char*)evbuffer_pullup(buf, size);
245 if (data)
246 reply->body = std::string(data, size);
247 evbuffer_drain(buf, size);
248 }
249 }
250
251 static void http_error_cb(enum evhttp_request_error err, void *ctx)
252 {
253 HTTPReply *reply = static_cast<HTTPReply*>(ctx);
254 reply->error = err;
255 }
256
257 /** Class that handles the conversion from a command-line to a JSON-RPC request,
258 * as well as converting back to a JSON object that can be shown as result.
259 */
260 class BaseRequestHandler
261 {
262 public:
263 virtual ~BaseRequestHandler() = default;
264 virtual UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) = 0;
265 virtual UniValue ProcessReply(const UniValue &batch_in) = 0;
266 };
267
268 /** Process addrinfo requests */
269 class AddrinfoRequestHandler : public BaseRequestHandler
270 {
271 private:
272 int8_t NetworkStringToId(const std::string& str) const
273 {
274 for (size_t i = 0; i < NETWORKS.size(); ++i) {
275 if (str == NETWORKS[i]) return i;
276 }
277 return UNKNOWN_NETWORK;
278 }
279
280 public:
281 UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
282 {
283 if (!args.empty()) {
284 throw std::runtime_error("-addrinfo takes no arguments");
285 }
286 UniValue params{RPCConvertValues("getnodeaddresses", std::vector<std::string>{{"0"}})};
287 return JSONRPCRequestObj("getnodeaddresses", params, 1);
288 }
289
290 UniValue ProcessReply(const UniValue& reply) override
291 {
292 if (!reply["error"].isNull()) return reply;
293 const std::vector<UniValue>& nodes{reply["result"].getValues()};
294 if (!nodes.empty() && nodes.at(0)["network"].isNull()) {
295 throw std::runtime_error("-addrinfo requires limenkad server to be running v0.21.1.knots or newer");
296 }
297 // Count the number of peers known to our node, by network.
298 std::array<uint64_t, NETWORKS.size()> counts{{}};
299 for (const UniValue& node : nodes) {
300 std::string network_name{node["network"].get_str()};
301 const int8_t network_id{NetworkStringToId(network_name)};
302 if (network_id == UNKNOWN_NETWORK) continue;
303 ++counts.at(network_id);
304 }
305 // Prepare result to return to user.
306 UniValue result{UniValue::VOBJ}, addresses{UniValue::VOBJ};
307 uint64_t total{0}; // Total address count
308 for (size_t i = 1; i < NETWORKS.size() - 1; ++i) {
309 addresses.pushKV(NETWORKS[i], counts.at(i));
310 total += counts.at(i);
311 }
312 addresses.pushKV("total", total);
313 result.pushKV("addresses_known", std::move(addresses));
314 return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V2);
315 }
316 };
317
318 /** Process getinfo requests */
319 class GetinfoRequestHandler: public BaseRequestHandler
320 {
321 public:
322 const int ID_NETWORKINFO = 0;
323 const int ID_BLOCKCHAININFO = 1;
324 const int ID_WALLETINFO = 2;
325 const int ID_BALANCES = 3;
326
327 /** Create a simulated `getinfo` request. */
328 UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
329 {
330 if (!args.empty()) {
331 throw std::runtime_error("-getinfo takes no arguments");
332 }
333 UniValue result(UniValue::VARR);
334 result.push_back(JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
335 result.push_back(JSONRPCRequestObj("getblockchaininfo", NullUniValue, ID_BLOCKCHAININFO));
336 result.push_back(JSONRPCRequestObj("getwalletinfo", NullUniValue, ID_WALLETINFO));
337 result.push_back(JSONRPCRequestObj("getbalances", NullUniValue, ID_BALANCES));
338 return result;
339 }
340
341 /** Collect values from the batch and form a simulated `getinfo` reply. */
342 UniValue ProcessReply(const UniValue &batch_in) override
343 {
344 UniValue result(UniValue::VOBJ);
345 const std::vector<UniValue> batch = JSONRPCProcessBatchReply(batch_in);
346 // Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass them on;
347 // getwalletinfo() and getbalances() are allowed to fail if there is no wallet.
348 if (!batch[ID_NETWORKINFO]["error"].isNull()) {
349 return batch[ID_NETWORKINFO];
350 }
351 if (!batch[ID_BLOCKCHAININFO]["error"].isNull()) {
352 return batch[ID_BLOCKCHAININFO];
353 }
354 result.pushKV("version", batch[ID_NETWORKINFO]["result"]["version"]);
355 result.pushKV("blocks", batch[ID_BLOCKCHAININFO]["result"]["blocks"]);
356 result.pushKV("headers", batch[ID_BLOCKCHAININFO]["result"]["headers"]);
357 result.pushKV("verificationprogress", batch[ID_BLOCKCHAININFO]["result"]["verificationprogress"]);
358 result.pushKV("timeoffset", batch[ID_NETWORKINFO]["result"]["timeoffset"]);
359
360 UniValue connections(UniValue::VOBJ);
361 connections.pushKV("in", batch[ID_NETWORKINFO]["result"]["connections_in"]);
362 connections.pushKV("out", batch[ID_NETWORKINFO]["result"]["connections_out"]);
363 connections.pushKV("total", batch[ID_NETWORKINFO]["result"]["connections"]);
364 result.pushKV("connections", std::move(connections));
365
366 result.pushKV("networks", batch[ID_NETWORKINFO]["result"]["networks"]);
367 result.pushKV("difficulty", batch[ID_BLOCKCHAININFO]["result"]["difficulty"]);
368 result.pushKV("chain", UniValue(batch[ID_BLOCKCHAININFO]["result"]["chain"]));
369 if (!batch[ID_WALLETINFO]["result"].isNull()) {
370 result.pushKV("has_wallet", true);
371 result.pushKV("keypoolsize", batch[ID_WALLETINFO]["result"]["keypoolsize"]);
372 result.pushKV("walletname", batch[ID_WALLETINFO]["result"]["walletname"]);
373 if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) {
374 result.pushKV("unlocked_until", batch[ID_WALLETINFO]["result"]["unlocked_until"]);
375 }
376 result.pushKV("paytxfee", batch[ID_WALLETINFO]["result"]["paytxfee"]);
377 }
378 if (!batch[ID_BALANCES]["result"].isNull()) {
379 result.pushKV("balance", batch[ID_BALANCES]["result"]["mine"]["trusted"]);
380 }
381 result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
382 result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
383 return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V2);
384 }
385 };
386
387 /** Process netinfo requests */
388 class NetinfoRequestHandler : public BaseRequestHandler
389 {
390 private:
391 std::array<std::array<uint16_t, NETWORKS.size() + 1>, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total)
392 uint8_t m_block_relay_peers_count{0};
393 uint8_t m_manual_peers_count{0};
394 int8_t NetworkStringToId(const std::string& str) const
395 {
396 for (size_t i = 0; i < NETWORKS.size(); ++i) {
397 if (str == NETWORKS[i]) return i;
398 }
399 return UNKNOWN_NETWORK;
400 }
401 uint8_t m_details_level{0}; //!< Optional user-supplied arg to set dashboard details level
402 bool DetailsRequested() const { return m_details_level > 0 && m_details_level < 5; }
403 bool IsAddressSelected() const { return m_details_level == 2 || m_details_level == 4; }
404 bool IsVersionSelected() const { return m_details_level == 3 || m_details_level == 4; }
405 bool m_outbound_only_selected{false};
406 bool m_is_asmap_on{false};
407 size_t m_max_addr_length{0};
408 size_t m_max_addr_processed_length{5};
409 size_t m_max_addr_rate_limited_length{6};
410 size_t m_max_age_length{5};
411 size_t m_max_id_length{2};
412 size_t m_max_services_length{6};
413 struct Peer {
414 std::string addr;
415 std::string sub_version;
416 std::string conn_type;
417 std::string network;
418 std::string age;
419 std::string services;
420 std::string transport_protocol_type;
421 double min_ping;
422 double ping;
423 int64_t addr_processed;
424 int64_t addr_rate_limited;
425 int64_t last_blck;
426 int64_t last_recv;
427 int64_t last_send;
428 int64_t last_trxn;
429 int id;
430 int cpu_load;
431 int mapped_as;
432 int version;
433 bool is_addr_relay_enabled;
434 bool is_bip152_hb_from;
435 bool is_bip152_hb_to;
436 bool is_outbound;
437 bool is_tx_relay;
438 bool operator<(const Peer& rhs) const { return std::tie(is_outbound, min_ping) < std::tie(rhs.is_outbound, rhs.min_ping); }
439 };
440 std::vector<Peer> m_peers;
441 std::string ChainToString() const
442 {
443 switch (gArgs.GetChainType()) {
444 case ChainType::TESTNET4:
445 return " testnet4";
446 case ChainType::TESTNET:
447 return " testnet";
448 case ChainType::SIGNET:
449 return " signet";
450 case ChainType::REGTEST:
451 return " regtest";
452 case ChainType::MAIN:
453 return "";
454 }
455 assert(false);
456 }
457 std::string PingTimeToString(double seconds) const
458 {
459 if (seconds < 0) return "";
460 const double milliseconds{round(1000 * seconds)};
461 return milliseconds > 999999 ? "-" : ToString(milliseconds);
462 }
463 std::string ConnectionTypeForNetinfo(const std::string& conn_type) const
464 {
465 if (conn_type == "outbound-full-relay") return "full";
466 if (conn_type == "block-relay-only") return "block";
467 if (conn_type == "manual" || conn_type == "feeler") return conn_type;
468 if (conn_type == "addr-fetch") return "addr";
469 return "";
470 }
471 std::string FormatServices(const UniValue& services)
472 {
473 std::string str;
474 for (size_t i = 0; i < services.size(); ++i) {
475 const std::string s{services[i].get_str()};
476 if (s == "NETWORK_LIMITED") {
477 str += 'l';
478 } else if (s == "P2P_V2") {
479 str += '2';
480 } else if (s == "UTREEXO") {
481 str += 't';
482 } else if (s == "UTREEXO_ARCHIVE") {
483 str += 'T';
484 } else if (s == "UTREEXO_TMP?") {
485 str += 'y';
486 } else if (s == "REDUCED_DATA?") {
487 str += '4';
488 } else {
489 str += ToLower(s[0]);
490 }
491 }
492 return str;
493 }
494 static std::string ServicesList(const UniValue& services)
495 {
496 std::string str{services.size() ? services[0].get_str() : ""};
497 for (size_t i{1}; i < services.size(); ++i) {
498 str += ", " + services[i].get_str();
499 }
500 for (auto& c: str) {
501 c = (c == '_' ? ' ' : ToLower(c));
502 }
503 return str;
504 }
505
506 public:
507 static constexpr int ID_PEERINFO = 0;
508 static constexpr int ID_NETWORKINFO = 1;
509
510 UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
511 {
512 if (!args.empty()) {
513 uint8_t n{0};
514 if (ParseUInt8(args.at(0), &n)) {
515 m_details_level = std::min(n, NETINFO_MAX_LEVEL);
516 } else {
517 throw std::runtime_error(strprintf("invalid -netinfo level argument: %s\nFor more information, run: limenka-cli -netinfo help", args.at(0)));
518 }
519 if (args.size() > 1) {
520 if (std::string_view s{args.at(1)}; n && (s == "o" || s == "outonly")) {
521 m_outbound_only_selected = true;
522 } else if (n) {
523 throw std::runtime_error(strprintf("invalid -netinfo outonly argument: %s\nFor more information, run: limenka-cli -netinfo help", s));
524 } else {
525 throw std::runtime_error(strprintf("invalid -netinfo outonly argument: %s\nThe outonly argument is only valid for a level greater than 0 (the first argument). For more information, run: limenka-cli -netinfo help", s));
526 }
527 }
528 }
529 UniValue result(UniValue::VARR);
530 result.push_back(JSONRPCRequestObj("getpeerinfo", NullUniValue, ID_PEERINFO));
531 result.push_back(JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
532 return result;
533 }
534
535 UniValue ProcessReply(const UniValue& batch_in) override
536 {
537 const std::vector<UniValue> batch{JSONRPCProcessBatchReply(batch_in)};
538 if (!batch[ID_PEERINFO]["error"].isNull()) return batch[ID_PEERINFO];
539 if (!batch[ID_NETWORKINFO]["error"].isNull()) return batch[ID_NETWORKINFO];
540
541 const UniValue& networkinfo{batch[ID_NETWORKINFO]["result"]};
542 if (networkinfo["version"].getInt<int>() < 209900) {
543 throw std::runtime_error("-netinfo requires limenkad server to be running v0.21.0 and up");
544 }
545 const int64_t time_now{TicksSinceEpoch<std::chrono::seconds>(CliClock::now())};
546
547 // Count peer connection totals, and if DetailsRequested(), store peer data in a vector of structs.
548 for (const UniValue& peer : batch[ID_PEERINFO]["result"].getValues()) {
549 const std::string network{peer["network"].get_str()};
550 const int8_t network_id{NetworkStringToId(network)};
551 if (network_id == UNKNOWN_NETWORK) continue;
552 const bool is_outbound{!peer["inbound"].get_bool()};
553 const bool is_tx_relay{peer["relaytxes"].isNull() ? true : peer["relaytxes"].get_bool()};
554 const std::string conn_type{peer["connection_type"].get_str()};
555 ++m_counts.at(is_outbound).at(network_id); // in/out by network
556 ++m_counts.at(is_outbound).at(NETWORKS.size()); // in/out overall
557 ++m_counts.at(2).at(network_id); // total by network
558 ++m_counts.at(2).at(NETWORKS.size()); // total overall
559 if (conn_type == "block-relay-only") ++m_block_relay_peers_count;
560 if (conn_type == "manual") ++m_manual_peers_count;
561 if (m_outbound_only_selected && !is_outbound) continue;
562 if (DetailsRequested()) {
563 // Push data for this peer to the peers vector.
564 const int peer_id{peer["id"].getInt<int>()};
565 const int mapped_as{peer["mapped_as"].isNull() ? 0 : peer["mapped_as"].getInt<int>()};
566 const int version{peer["version"].getInt<int>()};
567 const int64_t addr_processed{peer["addr_processed"].isNull() ? 0 : peer["addr_processed"].getInt<int64_t>()};
568 const int64_t addr_rate_limited{peer["addr_rate_limited"].isNull() ? 0 : peer["addr_rate_limited"].getInt<int64_t>()};
569 const int64_t conn_time{peer["conntime"].getInt<int64_t>()};
570 const int64_t last_blck{peer["last_block"].getInt<int64_t>()};
571 const int64_t last_recv{peer["lastrecv"].getInt<int64_t>()};
572 const int64_t last_send{peer["lastsend"].getInt<int64_t>()};
573 const int64_t last_trxn{peer["last_transaction"].getInt<int64_t>()};
574 const double min_ping{peer["minping"].isNull() ? -1 : peer["minping"].get_real()};
575 const double ping{peer["pingtime"].isNull() ? -1 : peer["pingtime"].get_real()};
576 const int cpu_load{peer["cpu_load"].isNull() ? -1 : static_cast<int>(round(peer["cpu_load"].get_real()))};
577 const std::string addr{peer["addr"].get_str()};
578 const std::string age{conn_time == 0 ? "" : ToString((time_now - conn_time) / 60)};
579 const std::string services{FormatServices(peer["servicesnames"])};
580 const std::string sub_version{peer["subver"].get_str()};
581 const std::string transport{peer["transport_protocol_type"].isNull() ? "v1" : peer["transport_protocol_type"].get_str()};
582 const bool is_addr_relay_enabled{peer["addr_relay_enabled"].isNull() ? false : peer["addr_relay_enabled"].get_bool()};
583 const bool is_bip152_hb_from{peer["bip152_hb_from"].get_bool()};
584 const bool is_bip152_hb_to{peer["bip152_hb_to"].get_bool()};
585 m_peers.push_back(Peer{.addr = addr,
586 .sub_version = sub_version,
587 .conn_type = conn_type,
588 .network = NETWORK_SHORT_NAMES[network_id],
589 .age = age,
590 .services = services,
591 .transport_protocol_type = transport,
592 .min_ping = min_ping,
593 .ping = ping,
594 .addr_processed = addr_processed,
595 .addr_rate_limited = addr_rate_limited,
596 .last_blck = last_blck,
597 .last_recv = last_recv,
598 .last_send = last_send,
599 .last_trxn = last_trxn,
600 .id = peer_id,
601 .cpu_load = cpu_load,
602 .mapped_as = mapped_as,
603 .version = version,
604 .is_addr_relay_enabled = is_addr_relay_enabled,
605 .is_bip152_hb_from = is_bip152_hb_from,
606 .is_bip152_hb_to = is_bip152_hb_to,
607 .is_outbound = is_outbound,
608 .is_tx_relay = is_tx_relay});
609 m_max_addr_length = std::max(addr.length() + 1, m_max_addr_length);
610 m_max_addr_processed_length = std::max(ToString(addr_processed).length(), m_max_addr_processed_length);
611 m_max_addr_rate_limited_length = std::max(ToString(addr_rate_limited).length(), m_max_addr_rate_limited_length);
612 m_max_age_length = std::max(age.length(), m_max_age_length);
613 m_max_id_length = std::max(ToString(peer_id).length(), m_max_id_length);
614 m_max_services_length = std::max(services.length(), m_max_services_length);
615 m_is_asmap_on |= (mapped_as != 0);
616 }
617 }
618
619 // Generate report header.
620 const std::string services{DetailsRequested() ? strprintf(" - services %s", FormatServices(networkinfo["localservicesnames"])) : ""};
621 std::string result{strprintf("%s client %s%s - server %i%s%s\n\n", CLIENT_NAME, FormatFullVersion(), ChainToString(), networkinfo["protocolversion"].getInt<int>(), networkinfo["subversion"].get_str(), services)};
622
623 // Report detailed peer connections list sorted by direction and minimum ping time.
624 if (DetailsRequested() && !m_peers.empty()) {
625 std::sort(m_peers.begin(), m_peers.end());
626 result += strprintf("<-> type net %*s v mping ping send recv txn blk hb %*s%*s cpu%*s ",
627 m_max_services_length, "serv",
628 m_max_addr_processed_length, "addrp",
629 m_max_addr_rate_limited_length, "addrl",
630 m_max_age_length, "age");
631 if (m_is_asmap_on) result += " asmap ";
632 result += strprintf("%*s %-*s%s\n", m_max_id_length, "id", IsAddressSelected() ? m_max_addr_length : 0, IsAddressSelected() ? "address" : "", IsVersionSelected() ? "version" : "");
633 for (const Peer& peer : m_peers) {
634 std::string version{ToString(peer.version) + peer.sub_version};
635 result += strprintf(
636 "%3s %6s %5s %*s %2s%7s%7s%5s%5s%5s%5s %2s %*s%*s%4s%*s%*i %*s %-*s%s\n",
637 peer.is_outbound ? "out" : "in",
638 ConnectionTypeForNetinfo(peer.conn_type),
639 peer.network,
640 m_max_services_length, // variable spacing
641 peer.services,
642 (peer.transport_protocol_type.size() == 2 && peer.transport_protocol_type[0] == 'v') ? peer.transport_protocol_type[1] : ' ',
643 PingTimeToString(peer.min_ping),
644 PingTimeToString(peer.ping),
645 peer.last_send ? ToString(time_now - peer.last_send) : "",
646 peer.last_recv ? ToString(time_now - peer.last_recv) : "",
647 peer.last_trxn ? ToString((time_now - peer.last_trxn) / 60) : peer.is_tx_relay ? "" : "*",
648 peer.last_blck ? ToString((time_now - peer.last_blck) / 60) : "",
649 strprintf("%s%s", peer.is_bip152_hb_to ? "." : " ", peer.is_bip152_hb_from ? "*" : " "),
650 m_max_addr_processed_length, // variable spacing
651 peer.addr_processed ? ToString(peer.addr_processed) : peer.is_addr_relay_enabled ? "" : ".",
652 m_max_addr_rate_limited_length, // variable spacing
653 peer.addr_rate_limited ? ToString(peer.addr_rate_limited) : "",
654 peer.cpu_load > 0 ? ToString(round(peer.cpu_load)) : "",
655 m_max_age_length, // variable spacing
656 peer.age,
657 m_is_asmap_on ? 7 : 0, // variable spacing
658 m_is_asmap_on && peer.mapped_as ? ToString(peer.mapped_as) : "",
659 m_max_id_length, // variable spacing
660 peer.id,
661 IsAddressSelected() ? m_max_addr_length : 0, // variable spacing
662 IsAddressSelected() ? peer.addr : "",
663 IsVersionSelected() && version != "0" ? version : "");
664 }
665 result += strprintf(" %*s ms ms sec sec min min ‰%*s\n\n", m_max_services_length, "", m_max_age_length, "min");
666 }
667
668 // Report peer connection totals by type.
669 result += " ";
670 std::vector<int8_t> reachable_networks;
671 for (const UniValue& network : networkinfo["networks"].getValues()) {
672 if (network["reachable"].get_bool()) {
673 const std::string& network_name{network["name"].get_str()};
674 const int8_t network_id{NetworkStringToId(network_name)};
675 if (network_id == UNKNOWN_NETWORK) continue;
676 result += strprintf("%8s", network_name); // column header
677 reachable_networks.push_back(network_id);
678 }
679 };
680
681 for (const size_t network_id : UNREACHABLE_NETWORK_IDS) {
682 if (m_counts.at(2).at(network_id) == 0) continue;
683 result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
684 reachable_networks.push_back(network_id);
685 }
686
687 result += " total block";
688 if (m_manual_peers_count) result += " manual";
689
690 const std::array rows{"in", "out", "total"};
691 for (size_t i = 0; i < rows.size(); ++i) {
692 result += strprintf("\n%-5s", rows[i]); // row header
693 for (int8_t n : reachable_networks) {
694 result += strprintf("%8i", m_counts.at(i).at(n)); // network peers count
695 }
696 result += strprintf(" %5i", m_counts.at(i).at(NETWORKS.size())); // total peers count
697 if (i == 1) { // the outbound row has two extra columns for block relay and manual peer counts
698 result += strprintf(" %5i", m_block_relay_peers_count);
699 if (m_manual_peers_count) result += strprintf(" %5i", m_manual_peers_count);
700 }
701 }
702
703 // Report local addresses, ports, and scores.
704 if (!DetailsRequested()) {
705 result += strprintf("\n\nLocal services: %s", ServicesList(networkinfo["localservicesnames"]));
706 }
707 result += "\n\nLocal addresses";
708 const std::vector<UniValue>& local_addrs{networkinfo["localaddresses"].getValues()};
709 if (local_addrs.empty()) {
710 result += ": n/a\n";
711 } else {
712 size_t max_addr_size{0};
713 for (const UniValue& addr : local_addrs) {
714 max_addr_size = std::max(addr["address"].get_str().length() + 1, max_addr_size);
715 }
716 for (const UniValue& addr : local_addrs) {
717 result += strprintf("\n%-*s port %6i score %6i", max_addr_size, addr["address"].get_str(), addr["port"].getInt<int>(), addr["score"].getInt<int>());
718 }
719 }
720
721 return JSONRPCReplyObj(UniValue{result}, NullUniValue, /*id=*/1, JSONRPCVersion::V2);
722 }
723
724 const std::string m_help_doc{
725 "-netinfo (level [outonly]) | help\n\n"
726 "Returns a network peer connections dashboard with information from the remote server.\n"
727 "This human-readable interface will change regularly and is not intended to be a stable API.\n"
728 "Under the hood, -netinfo fetches the data by calling getpeerinfo and getnetworkinfo.\n"
729 + strprintf("An optional argument from 0 to %d can be passed for different peers listings; values above %d up to 255 are parsed as %d.\n", NETINFO_MAX_LEVEL, NETINFO_MAX_LEVEL, NETINFO_MAX_LEVEL) +
730 "If that argument is passed, an optional additional \"outonly\" argument may be passed to obtain the listing with outbound peers only.\n"
731 "Pass \"help\" or \"h\" to see this detailed help documentation.\n"
732 "If more than two arguments are passed, only the first two are read and parsed.\n"
733 "Suggestion: use -netinfo with the Linux watch(1) command for a live dashboard; see example below.\n\n"
734 "Arguments:\n"
735 + strprintf("1. level (integer 0-%d, optional) Specify the info level of the peers dashboard (default 0):\n", NETINFO_MAX_LEVEL) +
736 " 0 - Peer counts for each reachable network as well as for block relay peers\n"
737 " and manual peers, and the list of local addresses and ports\n"
738 " 1 - Like 0 but preceded by a peers listing (without address and version columns)\n"
739 " 2 - Like 1 but with an address column\n"
740 " 3 - Like 1 but with a version column\n"
741 " 4 - Like 1 but with both address and version columns\n"
742 "2. outonly (\"outonly\" or \"o\", optional) Return the peers listing with outbound peers only, i.e. to save screen space\n"
743 " when a node has many inbound peers. Only valid if a level is passed.\n\n"
744 "help (\"help\" or \"h\", optional) Print this help documentation instead of the dashboard.\n\n"
745 "Result:\n\n"
746 + strprintf("* The peers listing in levels 1-%d displays all of the peers sorted by direction and minimum ping time:\n\n", NETINFO_MAX_LEVEL) +
747 " Column Description\n"
748 " ------ -----------\n"
749 " <-> Direction\n"
750 " \"in\" - inbound connections are those initiated by the peer\n"
751 " \"out\" - outbound connections are those initiated by us\n"
752 " type Type of peer connection\n"
753 " \"full\" - full relay, the default\n"
754 " \"block\" - block relay; like full relay but does not relay transactions or addresses\n"
755 " \"manual\" - peer we manually added using RPC addnode or the -addnode/-connect config options\n"
756 " \"feeler\" - short-lived connection for testing addresses\n"
757 " \"addr\" - address fetch; short-lived connection for requesting addresses\n"
758 " net Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", \"cjdns\", or \"npr\" (not publicly routable))\n"
759 " serv Services offered by the peer\n"
760 " \"n\" - NETWORK: peer can serve the full block chain\n"
761 " \"b\" - BLOOM: peer can handle bloom-filtered connections (see BIP 111)\n"
762 " \"w\" - WITNESS: peer can be asked for blocks and transactions with witness data (SegWit)\n"
763 " \"c\" - COMPACT_FILTERS: peer can handle basic block filter requests (see BIPs 157 and 158)\n"
764 " \"l\" - NETWORK_LIMITED: peer limited to serving only the last 288 blocks (~2 days)\n"
765 " \"2\" - P2P_V2: peer supports version 2 P2P transport protocol, as defined in BIP 324\n"
766 " \"t\" - UTREEXO peer can handle Utreexo proof requests for blocks it serves\n"
767 " \"T\" - UTREEXO_ARCHIVE peer can handle Utreexo proof requests for all historical blocks\n"
768 " \"y\" - UTREEXO_TMP? peer can handle Utreexo proof requests\n"
769 " \"r\" - REPLACE_BY_FEE? peer supports replacement of transactions without BIP 125 signalling\n"
770 " \"4\" - REDUCED_DATA? peer enforces the ReducedData SoftFork\n"
771 " \"m\" - MALICIOUS? peer openly seeks to aid in bypassing network policy/spam filters (OR to sabotage nodes that seek to)\n"
772 " \"u\" - UNKNOWN: unrecognized bit flag\n"
773 " v Version of transport protocol used for the connection\n"
774 " mping Minimum observed ping time, in milliseconds (ms)\n"
775 " ping Last observed ping time, in milliseconds (ms)\n"
776 " send Time since last message sent to the peer, in seconds\n"
777 " recv Time since last message received from the peer, in seconds\n"
778 " txn Time since last novel transaction received from the peer and accepted into our mempool, in minutes\n"
779 " \"*\" - we do not relay transactions to this peer (getpeerinfo \"relaytxes\" is false)\n"
780 " blk Time since last novel block passing initial validity checks received from the peer, in minutes\n"
781 " hb High-bandwidth BIP152 compact block relay\n"
782 " \".\" (to) - we selected the peer as a high-bandwidth peer\n"
783 " \"*\" (from) - the peer selected us as a high-bandwidth peer\n"
784 " addrp Total number of addresses processed, excluding those dropped due to rate limiting\n"
785 " \".\" - we do not relay addresses to this peer (getpeerinfo \"addr_relay_enabled\" is false)\n"
786 " addrl Total number of addresses dropped due to rate limiting\n"
787 " cpu CPU time processing messages to/from peer, per milles (‰) of age, rounded to nearest integer, if non-zero\n"
788 " age Duration of connection to the peer, in minutes\n"
789 " asmap Mapped AS (Autonomous System) number at the end of the BGP route to the peer, used for diversifying\n"
790 " peer selection (only displayed if the -asmap config option is set)\n"
791 " id Peer index, in increasing order of peer connections since node startup\n"
792 " address IP address and port of the peer\n"
793 " version Peer version and subversion concatenated, e.g. \"70016/limenka:21.0.0/\"\n\n"
794 "* The peer counts table displays the number of peers for each reachable network as well as\n"
795 " the number of block relay peers and manual peers.\n\n"
796 "* The local addresses table lists each local address broadcast by the node, the port, and the score.\n\n"
797 "Examples:\n\n"
798 "Peer counts table of reachable networks and list of local addresses\n"
799 "> limenka-cli -netinfo\n\n"
800 "The same, preceded by a peers listing without address and version columns\n"
801 "> limenka-cli -netinfo 1\n\n"
802 "Full dashboard\n"
803 + strprintf("> limenka-cli -netinfo %d\n\n", NETINFO_MAX_LEVEL) +
804 "Full dashboard, but with outbound peers only\n"
805 + strprintf("> limenka-cli -netinfo %d outonly\n\n", NETINFO_MAX_LEVEL) +
806 "Full live dashboard, adjust --interval or --no-title as needed (Linux)\n"
807 + strprintf("> watch --interval 1 --no-title limenka-cli -netinfo %d\n\n", NETINFO_MAX_LEVEL) +
808 "See this help\n"
809 "> limenka-cli -netinfo help\n"};
810 };
811
812 /** Process RPC generatetoaddress request. */
813 class GenerateToAddressRequestHandler : public BaseRequestHandler
814 {
815 public:
816 UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
817 {
818 address_str = args.at(1);
819 UniValue params{RPCConvertValues("generatetoaddress", args)};
820 return JSONRPCRequestObj("generatetoaddress", params, 1);
821 }
822
823 UniValue ProcessReply(const UniValue &reply) override
824 {
825 UniValue result(UniValue::VOBJ);
826 result.pushKV("address", address_str);
827 result.pushKV("blocks", reply.get_obj()["result"]);
828 return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V2);
829 }
830 protected:
831 std::string address_str;
832 };
833
834 /** Process default single requests */
835 class DefaultRequestHandler: public BaseRequestHandler {
836 public:
837 UniValue PrepareRequest(const std::string& method, const std::vector<std::string>& args) override
838 {
839 UniValue params;
840 if(gArgs.GetBoolArg("-named", DEFAULT_NAMED)) {
841 params = RPCConvertNamedValues(method, args);
842 } else {
843 params = RPCConvertValues(method, args);
844 }
845 return JSONRPCRequestObj(method, params, 1);
846 }
847
848 UniValue ProcessReply(const UniValue &reply) override
849 {
850 return reply.get_obj();
851 }
852 };
853
854 static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::optional<std::string>& rpcwallet = {})
855 {
856 std::string host;
857 // In preference order, we choose the following for the port:
858 // 1. -rpcport
859 // 2. port in -rpcconnect (ie following : in ipv4 or ]: in ipv6)
860 // 3. default port for chain
861 uint16_t port{BaseParams().RPCPort()};
862 {
863 uint16_t rpcconnect_port{0};
864 const std::string rpcconnect_str = gArgs.GetArg("-rpcconnect", DEFAULT_RPCCONNECT);
865 if (!SplitHostPort(rpcconnect_str, rpcconnect_port, host)) {
866 // Uses argument provided as-is
867 // (rather than value parsed)
868 // to aid the user in troubleshooting
869 throw std::runtime_error(strprintf("Invalid port provided in -rpcconnect: %s", rpcconnect_str));
870 } else {
871 if (rpcconnect_port != 0) {
872 // Use the valid port provided in rpcconnect
873 port = rpcconnect_port;
874 } // else, no port was provided in rpcconnect (continue using default one)
875 }
876
877 if (std::optional<std::string> rpcport_arg = gArgs.GetArg("-rpcport")) {
878 // -rpcport was specified
879 const uint16_t rpcport_int{ToIntegral<uint16_t>(rpcport_arg.value()).value_or(0)};
880 if (rpcport_int == 0) {
881 // Uses argument provided as-is
882 // (rather than value parsed)
883 // to aid the user in troubleshooting
884 throw std::runtime_error(strprintf("Invalid port provided in -rpcport: %s", rpcport_arg.value()));
885 }
886
887 // Use the valid port provided
888 port = rpcport_int;
889
890 // If there was a valid port provided in rpcconnect,
891 // rpcconnect_port is non-zero.
892 if (rpcconnect_port != 0) {
893 tfm::format(std::cerr, "Warning: Port specified in both -rpcconnect and -rpcport. Using -rpcport %u\n", port);
894 }
895 }
896 }
897
898 // Obtain event base
899 raii_event_base base = obtain_event_base();
900
901 // Synchronously look up hostname
902 raii_evhttp_connection evcon = obtain_evhttp_connection_base(base.get(), host, port);
903
904 // Set connection timeout
905 {
906 const int timeout = gArgs.GetIntArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT);
907 if (timeout > 0) {
908 evhttp_connection_set_timeout(evcon.get(), timeout);
909 } else {
910 // Indefinite request timeouts are not possible in libevent-http, so we
911 // set the timeout to a very long time period instead.
912
913 constexpr int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
914 evhttp_connection_set_timeout(evcon.get(), 5 * YEAR_IN_SECONDS);
915 }
916 }
917
918 HTTPReply response;
919 raii_evhttp_request req = obtain_evhttp_request(http_request_done, (void*)&response);
920 if (req == nullptr) {
921 throw std::runtime_error("create http request failed");
922 }
923
924 evhttp_request_set_error_cb(req.get(), http_error_cb);
925
926 // Get credentials
927 std::string strRPCUserColonPass;
928 bool failedToGetAuthCookie = false;
929 if (gArgs.GetArg("-rpcpassword", "") == "") {
930 // Try fall back to cookie-based authentication if no password is provided
931 if (!GetAuthCookie(&strRPCUserColonPass)) {
932 failedToGetAuthCookie = true;
933 }
934 } else {
935 strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
936 }
937
938 struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
939 assert(output_headers);
940 evhttp_add_header(output_headers, "Host", host.c_str());
941 evhttp_add_header(output_headers, "Connection", "close");
942 evhttp_add_header(output_headers, "Content-Type", "application/json");
943 evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());
944
945 // Attach request data
946 std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
947 struct evbuffer* output_buffer = evhttp_request_get_output_buffer(req.get());
948 assert(output_buffer);
949 evbuffer_add(output_buffer, strRequest.data(), strRequest.size());
950
951 // check if we should use a special wallet endpoint
952 std::string endpoint = "/";
953 if (rpcwallet) {
954 char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false);
955 if (encodedURI) {
956 endpoint = "/wallet/" + std::string(encodedURI);
957 free(encodedURI);
958 } else {
959 throw CConnectionFailed("uri-encode failed");
960 }
961 }
962 int r = evhttp_make_request(evcon.get(), req.get(), EVHTTP_REQ_POST, endpoint.c_str());
963 req.release(); // ownership moved to evcon in above call
964 if (r != 0) {
965 throw CConnectionFailed("send http request failed");
966 }
967
968 event_base_dispatch(base.get());
969
970 if (response.status == 0) {
971 std::string responseErrorMessage;
972 if (response.error != -1) {
973 responseErrorMessage = strprintf(" (error code %d - \"%s\")", response.error, http_errorstring(response.error));
974 }
975 throw CConnectionFailed(strprintf("Could not connect to the server %s:%d%s\n\n"
976 "Make sure the limenkad server is running and that you are connecting to the correct RPC port.\n"
977 "Use \"limenka-cli -help\" for more info.",
978 host, port, responseErrorMessage));
979 } else if (response.status == HTTP_UNAUTHORIZED) {
980 if (failedToGetAuthCookie) {
981 throw std::runtime_error(strprintf(
982 "Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set. See -rpcpassword and -stdinrpcpass. Configuration file: (%s)",
983 fs::PathToString(gArgs.GetConfigFilePath())));
984 } else {
985 throw std::runtime_error("Authorization failed: Incorrect rpcuser or rpcpassword");
986 }
987 } else if (response.status == HTTP_SERVICE_UNAVAILABLE) {
988 throw std::runtime_error(strprintf("Server response: %s", response.body));
989 } else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR)
990 throw std::runtime_error(strprintf("server returned HTTP error %d", response.status));
991 else if (response.body.empty())
992 throw std::runtime_error("no response from server");
993
994 // Parse reply
995 UniValue valReply(UniValue::VSTR);
996 if (!valReply.read(response.body))
997 throw std::runtime_error("couldn't parse reply from server");
998 UniValue reply = rh->ProcessReply(valReply);
999 if (reply.empty())
1000 throw std::runtime_error("expected reply to have result, error and id properties");
1001
1002 return reply;
1003 }
1004
1005 /**
1006 * ConnectAndCallRPC wraps CallRPC with -rpcwait and an exception handler.
1007 *
1008 * @param[in] rh Pointer to RequestHandler.
1009 * @param[in] strMethod Reference to const string method to forward to CallRPC.
1010 * @param[in] rpcwallet Reference to const optional string wallet name to forward to CallRPC.
1011 * @returns the RPC response as a UniValue object.
1012 * @throws a CConnectionFailed std::runtime_error if connection failed or RPC server still in warmup.
1013 */
1014 static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::optional<std::string>& rpcwallet = {})
1015 {
1016 UniValue response(UniValue::VOBJ);
1017 // Execute and handle connection failures with -rpcwait.
1018 const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
1019 const int timeout = gArgs.GetIntArg("-rpcwaittimeout", DEFAULT_WAIT_CLIENT_TIMEOUT);
1020 const auto deadline{std::chrono::steady_clock::now() + 1s * timeout};
1021
1022 do {
1023 try {
1024 response = CallRPC(rh, strMethod, args, rpcwallet);
1025 if (fWait) {
1026 const UniValue& error = response.find_value("error");
1027 if (!error.isNull() && error["code"].getInt<int>() == RPC_IN_WARMUP) {
1028 throw CConnectionFailed("server in warmup");
1029 }
1030 }
1031 break; // Connection succeeded, no need to retry.
1032 } catch (const CConnectionFailed& e) {
1033 if (fWait && (timeout <= 0 || std::chrono::steady_clock::now() < deadline)) {
1034 UninterruptibleSleep(1s);
1035 } else {
1036 throw CConnectionFailed(strprintf("timeout on transient error: %s", e.what()));
1037 }
1038 }
1039 } while (fWait);
1040 return response;
1041 }
1042
1043 /** Parse UniValue result to update the message to print to std::cout. */
1044 static void ParseResult(const UniValue& result, std::string& strPrint)
1045 {
1046 if (result.isNull()) return;
1047 strPrint = result.isStr() ? result.get_str() : result.write(2);
1048 }
1049
1050 /** Parse UniValue error to update the message to print to std::cerr and the code to return. */
1051 static void ParseError(const UniValue& error, std::string& strPrint, int& nRet)
1052 {
1053 if (error.isObject()) {
1054 const UniValue& err_code = error.find_value("code");
1055 const UniValue& err_msg = error.find_value("message");
1056 if (!err_code.isNull()) {
1057 strPrint = "error code: " + err_code.getValStr() + "\n";
1058 }
1059 if (err_msg.isStr()) {
1060 strPrint += ("error message:\n" + err_msg.get_str());
1061 }
1062 if (err_code.isNum() && err_code.getInt<int>() == RPC_WALLET_NOT_SPECIFIED) {
1063 strPrint += " Or for the CLI, specify the \"-rpcwallet=<walletname>\" option before the command";
1064 strPrint += " (run \"limenka-cli -h\" for help or \"limenka-cli listwallets\" to see which wallets are currently loaded).";
1065 }
1066 } else {
1067 strPrint = "error: " + error.write();
1068 }
1069 nRet = abs(error["code"].getInt<int>());
1070 }
1071
1072 static CAmount AmountFromValue(const UniValue& value)
1073 {
1074 int64_t amount_i64{0};
1075 if (!ParseFixedPoint(value.getValStr(), 8, &amount_i64))
1076 throw std::runtime_error("Invalid amount");
1077 CAmount amount{amount_i64};
1078 if (!MoneyRange(amount))
1079 throw std::runtime_error("Amount out of range");
1080 return amount;
1081 }
1082
1083 static UniValue ValueFromAmount(const CAmount& amount)
1084 {
1085 bool sign{amount < 0};
1086 CAmount n_abs{sign ? -amount : amount};
1087 int64_t quotient{n_abs / COIN};
1088 int64_t remainder{n_abs % COIN};
1089 return UniValue(UniValue::VNUM, strprintf("%s%d.%08d", sign ? "-" : "", quotient, remainder));
1090 }
1091
1092 /**
1093 * GetWalletBalances calls listwallets; if more than one wallet is loaded, it
1094 * then fetches mine.trusted balances for each loaded wallet and pushes all the
1095 * balances, followed by the total balance, to `result`.
1096 *
1097 * @param result Reference to UniValue object the wallet names and balances are pushed to.
1098 */
1099 static void GetWalletBalances(UniValue& result)
1100 {
1101 DefaultRequestHandler rh;
1102 const UniValue listwallets = ConnectAndCallRPC(&rh, "listwallets", /* args=*/{});
1103 if (!listwallets.find_value("error").isNull()) return;
1104 const UniValue& wallets = listwallets.find_value("result");
1105 if (wallets.size() <= 1) return;
1106
1107 UniValue balances(UniValue::VOBJ);
1108 CAmount total_balance{0};
1109 for (const UniValue& wallet : wallets.getValues()) {
1110 const std::string& wallet_name = wallet.get_str();
1111 const UniValue getbalances = ConnectAndCallRPC(&rh, "getbalances", /* args=*/{}, wallet_name);
1112 if (!getbalances.find_value("error").isNull()) continue;
1113 const UniValue& balance = getbalances.find_value("result")["mine"]["trusted"];
1114 total_balance += AmountFromValue(balance);
1115 balances.pushKV(wallet_name, balance);
1116 }
1117 result.pushKV("balances", std::move(balances));
1118 result.pushKV("total_balance", ValueFromAmount(total_balance));
1119 }
1120
1121 /**
1122 * GetProgressBar constructs a progress bar with 5% intervals.
1123 *
1124 * @param[in] progress The proportion of the progress bar to be filled between 0 and 1.
1125 * @param[out] progress_bar String representation of the progress bar.
1126 */
1127 static void GetProgressBar(double progress, std::string& progress_bar)
1128 {
1129 if (progress < 0 || progress > 1) return;
1130
1131 static constexpr double INCREMENT{0.05};
1132 static const std::string COMPLETE_BAR{"\u2592"};
1133 static const std::string INCOMPLETE_BAR{"\u2591"};
1134
1135 for (int i = 0; i < progress / INCREMENT; ++i) {
1136 progress_bar += COMPLETE_BAR;
1137 }
1138
1139 for (int i = 0; i < (1 - progress) / INCREMENT; ++i) {
1140 progress_bar += INCOMPLETE_BAR;
1141 }
1142 }
1143
1144 /**
1145 * ParseGetInfoResult takes in -getinfo result in UniValue object and parses it
1146 * into a user friendly UniValue string to be printed on the console.
1147 * @param[out] result Reference to UniValue result containing the -getinfo output.
1148 */
1149 static void ParseGetInfoResult(UniValue& result)
1150 {
1151 if (!result.find_value("error").isNull()) return;
1152
1153 std::string RESET, GREEN, BLUE, YELLOW, MAGENTA, CYAN;
1154 bool should_colorize = false;
1155
1156 #ifndef WIN32
1157 if (isatty(fileno(stdout))) {
1158 // By default, only print colored text if OS is not WIN32 and stdout is connected to a terminal.
1159 should_colorize = true;
1160 }
1161 #endif
1162
1163 if (gArgs.IsArgSet("-color")) {
1164 const std::string color{gArgs.GetArg("-color", DEFAULT_COLOR_SETTING)};
1165 if (color == "always") {
1166 should_colorize = true;
1167 } else if (color == "never") {
1168 should_colorize = false;
1169 } else if (color != "auto") {
1170 throw std::runtime_error("Invalid value for -color option. Valid values: always, auto, never.");
1171 }
1172 }
1173
1174 if (should_colorize) {
1175 RESET = "\x1B[0m";
1176 GREEN = "\x1B[32m";
1177 BLUE = "\x1B[34m";
1178 YELLOW = "\x1B[33m";
1179 MAGENTA = "\x1B[35m";
1180 CYAN = "\x1B[36m";
1181 }
1182
1183 std::string result_string = strprintf("%sChain: %s%s\n", BLUE, result["chain"].getValStr(), RESET);
1184 result_string += strprintf("Blocks: %s\n", result["blocks"].getValStr());
1185 result_string += strprintf("Headers: %s\n", result["headers"].getValStr());
1186
1187 const double ibd_progress{result["verificationprogress"].get_real()};
1188 std::string ibd_progress_bar;
1189 // Display the progress bar only if IBD progress is less than 99%
1190 if (ibd_progress < 0.99) {
1191 GetProgressBar(ibd_progress, ibd_progress_bar);
1192 // Add padding between progress bar and IBD progress
1193 ibd_progress_bar += " ";
1194 }
1195
1196 result_string += strprintf("Verification progress: %s%.4f%%\n", ibd_progress_bar, ibd_progress * 100);
1197 result_string += strprintf("Difficulty: %s\n\n", result["difficulty"].getValStr());
1198
1199 result_string += strprintf(
1200 "%sNetwork: in %s, out %s, total %s%s\n",
1201 GREEN,
1202 result["connections"]["in"].getValStr(),
1203 result["connections"]["out"].getValStr(),
1204 result["connections"]["total"].getValStr(),
1205 RESET);
1206 result_string += strprintf("Version: %s\n", result["version"].getValStr());
1207 result_string += strprintf("Time offset (s): %s\n", result["timeoffset"].getValStr());
1208
1209 // proxies
1210 std::map<std::string, std::vector<std::string>> proxy_networks;
1211 std::vector<std::string> ordered_proxies;
1212
1213 for (const UniValue& network : result["networks"].getValues()) {
1214 const std::string proxy = network["proxy"].getValStr();
1215 if (proxy.empty()) continue;
1216 // Add proxy to ordered_proxy if has not been processed
1217 if (proxy_networks.find(proxy) == proxy_networks.end()) ordered_proxies.push_back(proxy);
1218
1219 proxy_networks[proxy].push_back(network["name"].getValStr());
1220 }
1221
1222 std::vector<std::string> formatted_proxies;
1223 formatted_proxies.reserve(ordered_proxies.size());
1224 for (const std::string& proxy : ordered_proxies) {
1225 formatted_proxies.emplace_back(strprintf("%s (%s)", proxy, Join(proxy_networks.find(proxy)->second, ", ")));
1226 }
1227 result_string += strprintf("Proxies: %s\n", formatted_proxies.empty() ? "n/a" : Join(formatted_proxies, ", "));
1228
1229 result_string += strprintf("Min tx relay fee rate (%s/kvB): %s\n\n", CURRENCY_UNIT, result["relayfee"].getValStr());
1230
1231 if (!result["has_wallet"].isNull()) {
1232 const std::string walletname = result["walletname"].getValStr();
1233 result_string += strprintf("%sWallet: %s%s\n", MAGENTA, walletname.empty() ? "\"\"" : walletname, RESET);
1234
1235 result_string += strprintf("Keypool size: %s\n", result["keypoolsize"].getValStr());
1236 if (!result["unlocked_until"].isNull()) {
1237 result_string += strprintf("Unlocked until: %s\n", result["unlocked_until"].getValStr());
1238 }
1239 result_string += strprintf("Transaction fee rate (-paytxfee) (%s/kvB): %s\n\n", CURRENCY_UNIT, result["paytxfee"].getValStr());
1240 }
1241 if (!result["balance"].isNull()) {
1242 result_string += strprintf("%sBalance:%s %s\n\n", CYAN, RESET, result["balance"].getValStr());
1243 }
1244
1245 if (!result["balances"].isNull()) {
1246 result_string += strprintf("%sBalances%s\n", CYAN, RESET);
1247
1248 size_t max_balance_length{10};
1249
1250 for (const std::string& wallet : result["balances"].getKeys()) {
1251 max_balance_length = std::max(result["balances"][wallet].getValStr().length(), max_balance_length);
1252 }
1253
1254 for (const std::string& wallet : result["balances"].getKeys()) {
1255 result_string += strprintf("%*s %s\n",
1256 max_balance_length,
1257 result["balances"][wallet].getValStr(),
1258 wallet.empty() ? "\"\"" : wallet);
1259 }
1260 result_string += "\n";
1261 result_string += strprintf("%sTotal balance:%s %s\n\n", CYAN, RESET, result["total_balance"].getValStr());
1262 }
1263
1264 const std::string warnings{result["warnings"].getValStr()};
1265 result_string += strprintf("%sWarnings:%s %s", YELLOW, RESET, warnings.empty() ? "(none)" : warnings);
1266
1267 result.setStr(result_string);
1268 }
1269
1270 /**
1271 * Call RPC getnewaddress.
1272 * @returns getnewaddress response as a UniValue object.
1273 */
1274 static UniValue GetNewAddress()
1275 {
1276 DefaultRequestHandler rh;
1277 return ConnectAndCallRPC(&rh, "getnewaddress", /* args=*/{}, RpcWalletName(gArgs));
1278 }
1279
1280 /**
1281 * Check bounds and set up args for RPC generatetoaddress params: nblocks, address, maxtries.
1282 * @param[in] address Reference to const string address to insert into the args.
1283 * @param args Reference to vector of string args to modify.
1284 */
1285 static void SetGenerateToAddressArgs(const std::string& address, std::vector<std::string>& args)
1286 {
1287 if (args.size() > 2) throw std::runtime_error("too many arguments (maximum 2 for nblocks and maxtries)");
1288 if (args.size() == 0) {
1289 args.emplace_back(DEFAULT_NBLOCKS);
1290 } else if (args.at(0) == "0") {
1291 throw std::runtime_error("the first argument (number of blocks to generate, default: " + DEFAULT_NBLOCKS + ") must be an integer value greater than zero");
1292 }
1293 args.emplace(args.begin() + 1, address);
1294 }
1295
1296 static int CommandLineRPC(int argc, char *argv[])
1297 {
1298 std::string strPrint;
1299 int nRet = 0;
1300 try {
1301 // Skip switches
1302 while (argc > 1 && IsSwitchChar(argv[1][0])) {
1303 argc--;
1304 argv++;
1305 }
1306 std::string rpcPass;
1307 if (gArgs.GetBoolArg("-stdinrpcpass", false)) {
1308 NO_STDIN_ECHO();
1309 if (!StdinReady()) {
1310 fputs("RPC password> ", stderr);
1311 fflush(stderr);
1312 }
1313 if (!std::getline(std::cin, rpcPass)) {
1314 throw std::runtime_error("-stdinrpcpass specified but failed to read from standard input");
1315 }
1316 if (StdinTerminal()) {
1317 fputc('\n', stdout);
1318 }
1319 gArgs.ForceSetArg("-rpcpassword", rpcPass);
1320 }
1321 std::vector<std::string> args = std::vector<std::string>(&argv[1], &argv[argc]);
1322 if (gArgs.GetBoolArg("-stdinwalletpassphrase", false)) {
1323 NO_STDIN_ECHO();
1324 std::string walletPass;
1325 if (args.size() < 1 || args[0].substr(0, 16) != "walletpassphrase") {
1326 throw std::runtime_error("-stdinwalletpassphrase is only applicable for walletpassphrase(change)");
1327 }
1328 if (!StdinReady()) {
1329 fputs("Wallet passphrase> ", stderr);
1330 fflush(stderr);
1331 }
1332 if (!std::getline(std::cin, walletPass)) {
1333 throw std::runtime_error("-stdinwalletpassphrase specified but failed to read from standard input");
1334 }
1335 if (StdinTerminal()) {
1336 fputc('\n', stdout);
1337 }
1338 args.insert(args.begin() + 1, walletPass);
1339 }
1340 if (gArgs.GetBoolArg("-stdin", false)) {
1341 // Read one arg per line from stdin and append
1342 std::string line;
1343 while (std::getline(std::cin, line)) {
1344 args.push_back(line);
1345 }
1346 if (StdinTerminal()) {
1347 fputc('\n', stdout);
1348 }
1349 }
1350 gArgs.CheckMultipleCLIArgs();
1351 std::unique_ptr<BaseRequestHandler> rh;
1352 std::string method;
1353 if (gArgs.GetBoolArg("-getinfo", false)) {
1354 rh.reset(new GetinfoRequestHandler());
1355 } else if (gArgs.GetBoolArg("-netinfo", false)) {
1356 if (!args.empty() && (args.at(0) == "h" || args.at(0) == "help")) {
1357 tfm::format(std::cout, "%s\n", NetinfoRequestHandler().m_help_doc);
1358 return 0;
1359 }
1360 rh.reset(new NetinfoRequestHandler());
1361 } else if (gArgs.GetBoolArg("-generate", false)) {
1362 const UniValue getnewaddress{GetNewAddress()};
1363 const UniValue& error{getnewaddress.find_value("error")};
1364 if (error.isNull()) {
1365 SetGenerateToAddressArgs(getnewaddress.find_value("result").get_str(), args);
1366 rh.reset(new GenerateToAddressRequestHandler());
1367 } else {
1368 ParseError(error, strPrint, nRet);
1369 }
1370 } else if (gArgs.GetBoolArg("-addrinfo", false)) {
1371 rh.reset(new AddrinfoRequestHandler());
1372 } else {
1373 rh.reset(new DefaultRequestHandler());
1374 if (args.size() < 1) {
1375 throw std::runtime_error("too few parameters (need at least command)");
1376 }
1377 method = args[0];
1378 args.erase(args.begin()); // Remove trailing method name from arguments vector
1379 }
1380 if (nRet == 0) {
1381 // Perform RPC call
1382 const std::optional<std::string> wallet_name{RpcWalletName(gArgs)};
1383 const UniValue reply = ConnectAndCallRPC(rh.get(), method, args, wallet_name);
1384
1385 // Parse reply
1386 UniValue result = reply.find_value("result");
1387 const UniValue& error = reply.find_value("error");
1388 if (error.isNull()) {
1389 if (gArgs.GetBoolArg("-getinfo", false)) {
1390 if (!wallet_name) {
1391 GetWalletBalances(result); // fetch multiwallet balances and append to result
1392 }
1393 ParseGetInfoResult(result);
1394 }
1395
1396 ParseResult(result, strPrint);
1397 } else {
1398 ParseError(error, strPrint, nRet);
1399 }
1400 }
1401 } catch (const std::exception& e) {
1402 strPrint = std::string("error: ") + e.what();
1403 nRet = EXIT_FAILURE;
1404 } catch (...) {
1405 PrintExceptionContinue(nullptr, "CommandLineRPC()");
1406 throw;
1407 }
1408
1409 if (strPrint != "") {
1410 tfm::format(nRet == 0 ? std::cout : std::cerr, "%s\n", strPrint);
1411 }
1412 return nRet;
1413 }
1414
1415 MAIN_FUNCTION
1416 {
1417 #ifdef WIN32
1418 common::WinCmdLineArgs winArgs;
1419 std::tie(argc, argv) = winArgs.get();
1420 #endif
1421 SetupEnvironment();
1422 if (!SetupNetworking()) {
1423 tfm::format(std::cerr, "Error: Initializing networking failed\n");
1424 return EXIT_FAILURE;
1425 }
1426 event_set_log_callback(&libevent_log_cb);
1427
1428 try {
1429 int ret = AppInitRPC(argc, argv);
1430 if (ret != CONTINUE_EXECUTION)
1431 return ret;
1432 }
1433 catch (const std::exception& e) {
1434 PrintExceptionContinue(&e, "AppInitRPC()");
1435 return EXIT_FAILURE;
1436 } catch (...) {
1437 PrintExceptionContinue(nullptr, "AppInitRPC()");
1438 return EXIT_FAILURE;
1439 }
1440
1441 int ret = EXIT_FAILURE;
1442 try {
1443 ret = CommandLineRPC(argc, argv);
1444 }
1445 catch (const std::exception& e) {
1446 PrintExceptionContinue(&e, "CommandLineRPC()");
1447 } catch (...) {
1448 PrintExceptionContinue(nullptr, "CommandLineRPC()");
1449 }
1450 return ret;
1451 }
1452