/tmp/bitcoin/src/rpc/node.cpp
Line | Count | Source |
1 | | // Copyright (c) 2010 Satoshi Nakamoto |
2 | | // Copyright (c) 2009-present The Bitcoin Core 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 <bitcoin-build-config.h> // IWYU pragma: keep |
7 | | |
8 | | #include <chainparams.h> |
9 | | #include <httpserver.h> |
10 | | #include <index/blockfilterindex.h> |
11 | | #include <index/coinstatsindex.h> |
12 | | #include <index/txindex.h> |
13 | | #include <index/txospenderindex.h> |
14 | | #include <interfaces/chain.h> |
15 | | #include <interfaces/echo.h> |
16 | | #include <interfaces/init.h> |
17 | | #include <interfaces/ipc.h> |
18 | | #include <kernel/cs_main.h> |
19 | | #include <logging.h> |
20 | | #include <node/context.h> |
21 | | #include <rpc/server.h> |
22 | | #include <rpc/server_util.h> |
23 | | #include <rpc/util.h> |
24 | | #include <scheduler.h> |
25 | | #include <tinyformat.h> |
26 | | #include <univalue.h> |
27 | | #include <util/any.h> |
28 | | #include <util/check.h> |
29 | | #include <util/time.h> |
30 | | |
31 | | #include <cstdint> |
32 | | #include <limits> |
33 | | #ifdef HAVE_MALLOC_INFO |
34 | | #include <malloc.h> |
35 | | #endif |
36 | | #include <string_view> |
37 | | |
38 | | using node::NodeContext; |
39 | | |
40 | | static RPCMethod setmocktime() |
41 | 3.70k | { |
42 | 3.70k | return RPCMethod{ |
43 | 3.70k | "setmocktime", |
44 | 3.70k | "Set the local time to given timestamp (-regtest only)\n", |
45 | 3.70k | { |
46 | 3.70k | {"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, UNIX_EPOCH_TIME + "\n" |
47 | 3.70k | "Pass 0 to go back to using the system time."}, |
48 | 3.70k | }, |
49 | 3.70k | RPCResult{RPCResult::Type::NONE, "", ""}, |
50 | 3.70k | RPCExamples{""}, |
51 | 3.70k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
52 | 3.70k | { |
53 | 1.36k | if (!Params().IsMockableChain()) { |
54 | 0 | throw std::runtime_error("setmocktime is for regression testing (-regtest mode) only"); |
55 | 0 | } |
56 | | |
57 | | // For now, don't change mocktime if we're in the middle of validation, as |
58 | | // this could have an effect on mempool time-based eviction, as well as |
59 | | // IsCurrentForFeeEstimation() and IsInitialBlockDownload(). |
60 | | // TODO: figure out the right way to synchronize around mocktime, and |
61 | | // ensure all call sites of GetTime() are accessing this safely. |
62 | 1.36k | LOCK(cs_main); |
63 | | |
64 | 1.36k | const int64_t time{request.params[0].getInt<int64_t>()}; |
65 | | // block timestamps are uint32_t, so mocking time beyond that is meaningless for anything |
66 | | // consensus-related and can cause integer overflow/truncation issues in time arithmetic. |
67 | 1.36k | constexpr int64_t max_time{std::numeric_limits<uint32_t>::max()}; |
68 | 1.36k | if (time < 0 || time > max_time) { |
69 | 4 | throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Mocktime must be in the range [0, %s], not %s.", max_time, time)); |
70 | 4 | } |
71 | | |
72 | 1.35k | SetMockTime(time); |
73 | 1.35k | const NodeContext& node_context{EnsureAnyNodeContext(request.context)}; |
74 | 1.35k | for (const auto& chain_client : node_context.chain_clients) { |
75 | 164 | chain_client->setMockTime(time); |
76 | 164 | } |
77 | | |
78 | 1.35k | return UniValue::VNULL; |
79 | 1.36k | }, |
80 | 3.70k | }; |
81 | 3.70k | } |
82 | | |
83 | | static RPCMethod mockscheduler() |
84 | 2.35k | { |
85 | 2.35k | return RPCMethod{ |
86 | 2.35k | "mockscheduler", |
87 | 2.35k | "Bump the scheduler into the future (-regtest only)\n", |
88 | 2.35k | { |
89 | 2.35k | {"delta_time", RPCArg::Type::NUM, RPCArg::Optional::NO, "Number of seconds to forward the scheduler into the future." }, |
90 | 2.35k | }, |
91 | 2.35k | RPCResult{RPCResult::Type::NONE, "", ""}, |
92 | 2.35k | RPCExamples{""}, |
93 | 2.35k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
94 | 2.35k | { |
95 | 12 | if (!Params().IsMockableChain()) { |
96 | 0 | throw std::runtime_error("mockscheduler is for regression testing (-regtest mode) only"); |
97 | 0 | } |
98 | | |
99 | 12 | int64_t delta_seconds = request.params[0].getInt<int64_t>(); |
100 | 12 | if (delta_seconds <= 0 || delta_seconds > 3600) { |
101 | 0 | throw std::runtime_error("delta_time must be between 1 and 3600 seconds (1 hr)"); |
102 | 0 | } |
103 | | |
104 | 12 | const NodeContext& node_context{EnsureAnyNodeContext(request.context)}; |
105 | 12 | CHECK_NONFATAL(node_context.scheduler)->MockForward(std::chrono::seconds{delta_seconds}); |
106 | 12 | CHECK_NONFATAL(node_context.validation_signals)->SyncWithValidationInterfaceQueue(); |
107 | 12 | for (const auto& chain_client : node_context.chain_clients) { |
108 | 7 | chain_client->schedulerMockForward(std::chrono::seconds(delta_seconds)); |
109 | 7 | } |
110 | | |
111 | 12 | return UniValue::VNULL; |
112 | 12 | }, |
113 | 2.35k | }; |
114 | 2.35k | } |
115 | | |
116 | | static UniValue RPCLockedMemoryInfo() |
117 | 1 | { |
118 | 1 | LockedPool::Stats stats = LockedPoolManager::Instance().stats(); |
119 | 1 | UniValue obj(UniValue::VOBJ); |
120 | 1 | obj.pushKV("used", stats.used); |
121 | 1 | obj.pushKV("free", stats.free); |
122 | 1 | obj.pushKV("total", stats.total); |
123 | 1 | obj.pushKV("locked", stats.locked); |
124 | 1 | obj.pushKV("chunks_used", stats.chunks_used); |
125 | 1 | obj.pushKV("chunks_free", stats.chunks_free); |
126 | 1 | return obj; |
127 | 1 | } |
128 | | |
129 | | #ifdef HAVE_MALLOC_INFO |
130 | | static std::string RPCMallocInfo() |
131 | 1 | { |
132 | 1 | char *ptr = nullptr; |
133 | 1 | size_t size = 0; |
134 | 1 | FILE *f = open_memstream(&ptr, &size); |
135 | 1 | if (f) { |
136 | 1 | malloc_info(0, f); |
137 | 1 | fclose(f); |
138 | 1 | if (ptr) { |
139 | 1 | std::string rv(ptr, size); |
140 | 1 | free(ptr); |
141 | 1 | return rv; |
142 | 1 | } |
143 | 1 | } |
144 | 0 | return ""; |
145 | 1 | } |
146 | | #endif |
147 | | |
148 | | static RPCMethod getmemoryinfo() |
149 | 2.35k | { |
150 | | /* Please, avoid using the word "pool" here in the RPC interface or help, |
151 | | * as users will undoubtedly confuse it with the other "memory pool" |
152 | | */ |
153 | 2.35k | return RPCMethod{"getmemoryinfo", |
154 | 2.35k | "Returns an object containing information about memory usage.\n", |
155 | 2.35k | { |
156 | 2.35k | {"mode", RPCArg::Type::STR, RPCArg::Default{"stats"}, "determines what kind of information is returned.\n" |
157 | 2.35k | " - \"stats\" returns general statistics about memory usage in the daemon.\n" |
158 | 2.35k | " - \"mallocinfo\" returns an XML string describing low-level heap state (only available if compiled with glibc)."}, |
159 | 2.35k | }, |
160 | 2.35k | { |
161 | 2.35k | RPCResult{"mode \"stats\"", |
162 | 2.35k | RPCResult::Type::OBJ, "", "", |
163 | 2.35k | { |
164 | 2.35k | {RPCResult::Type::OBJ, "locked", "Information about locked memory manager", |
165 | 2.35k | { |
166 | 2.35k | {RPCResult::Type::NUM, "used", "Number of bytes used"}, |
167 | 2.35k | {RPCResult::Type::NUM, "free", "Number of bytes available in current arenas"}, |
168 | 2.35k | {RPCResult::Type::NUM, "total", "Total number of bytes managed"}, |
169 | 2.35k | {RPCResult::Type::NUM, "locked", "Amount of bytes that succeeded locking. If this number is smaller than total, locking pages failed at some point and key data could be swapped to disk."}, |
170 | 2.35k | {RPCResult::Type::NUM, "chunks_used", "Number allocated chunks"}, |
171 | 2.35k | {RPCResult::Type::NUM, "chunks_free", "Number unused chunks"}, |
172 | 2.35k | }}, |
173 | 2.35k | } |
174 | 2.35k | }, |
175 | 2.35k | RPCResult{"mode \"mallocinfo\"", |
176 | 2.35k | RPCResult::Type::STR, "", "\"<malloc version=\"1\">...\"" |
177 | 2.35k | }, |
178 | 2.35k | }, |
179 | 2.35k | RPCExamples{ |
180 | 2.35k | HelpExampleCli("getmemoryinfo", "") |
181 | 2.35k | + HelpExampleRpc("getmemoryinfo", "") |
182 | 2.35k | }, |
183 | 2.35k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
184 | 2.35k | { |
185 | 3 | auto mode{self.Arg<std::string_view>("mode")}; |
186 | 3 | if (mode == "stats") { |
187 | 1 | UniValue obj(UniValue::VOBJ); |
188 | 1 | obj.pushKV("locked", RPCLockedMemoryInfo()); |
189 | 1 | return obj; |
190 | 2 | } else if (mode == "mallocinfo") { |
191 | 1 | #ifdef HAVE_MALLOC_INFO |
192 | 1 | return RPCMallocInfo(); |
193 | | #else |
194 | | throw JSONRPCError(RPC_INVALID_PARAMETER, "mallocinfo mode not available"); |
195 | | #endif |
196 | 1 | } else { |
197 | 1 | throw JSONRPCError(RPC_INVALID_PARAMETER, tfm::format("unknown mode %s", mode)); |
198 | 1 | } |
199 | 3 | }, |
200 | 2.35k | }; |
201 | 2.35k | } |
202 | | |
203 | 2 | static void EnableOrDisableLogCategories(UniValue cats, bool enable) { |
204 | 2 | cats = cats.get_array(); |
205 | 4 | for (unsigned int i = 0; i < cats.size(); ++i) { |
206 | 2 | std::string cat = cats[i].get_str(); |
207 | | |
208 | 2 | bool success; |
209 | 2 | if (enable) { |
210 | 1 | success = LogInstance().EnableCategory(cat); |
211 | 1 | } else { |
212 | 1 | success = LogInstance().DisableCategory(cat); |
213 | 1 | } |
214 | | |
215 | 2 | if (!success) { |
216 | 0 | throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown logging category " + cat); |
217 | 0 | } |
218 | 2 | } |
219 | 2 | } |
220 | | |
221 | | static RPCMethod logging() |
222 | 2.36k | { |
223 | 2.36k | return RPCMethod{"logging", |
224 | 2.36k | "Gets and sets the logging configuration.\n" |
225 | 2.36k | "When called without an argument, returns the list of categories with status that are currently being debug logged or not.\n" |
226 | 2.36k | "When called with arguments, adds or removes categories from debug logging and return the lists above.\n" |
227 | 2.36k | "The arguments are evaluated in order \"include\", \"exclude\".\n" |
228 | 2.36k | "If an item is both included and excluded, it will thus end up being excluded.\n" |
229 | 2.36k | "The valid logging categories are: " + LogInstance().LogCategoriesString() + "\n" |
230 | 2.36k | "In addition, the following are available as category names with special meanings:\n" |
231 | 2.36k | " - \"all\", \"1\" : represent all logging categories.\n" |
232 | 2.36k | , |
233 | 2.36k | { |
234 | 2.36k | {"include", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The categories to add to debug logging", |
235 | 2.36k | { |
236 | 2.36k | {"include_category", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "the valid logging category"}, |
237 | 2.36k | }}, |
238 | 2.36k | {"exclude", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The categories to remove from debug logging", |
239 | 2.36k | { |
240 | 2.36k | {"exclude_category", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "the valid logging category"}, |
241 | 2.36k | }}, |
242 | 2.36k | }, |
243 | 2.36k | RPCResult{ |
244 | 2.36k | RPCResult::Type::OBJ_DYN, "", "keys are the logging categories, and values indicates its status", |
245 | 2.36k | { |
246 | 2.36k | {RPCResult::Type::BOOL, "category", "if being debug logged or not. false:inactive, true:active"}, |
247 | 2.36k | } |
248 | 2.36k | }, |
249 | 2.36k | RPCExamples{ |
250 | 2.36k | HelpExampleCli("logging", "\"[\\\"all\\\"]\" \"[\\\"http\\\"]\"") |
251 | 2.36k | + HelpExampleRpc("logging", "[\"all\"], [\"libevent\"]") |
252 | 2.36k | }, |
253 | 2.36k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
254 | 2.36k | { |
255 | 10 | BCLog::CategoryMask original_log_categories = LogInstance().GetCategoryMask(); |
256 | 10 | if (request.params[0].isArray()) { |
257 | 1 | EnableOrDisableLogCategories(request.params[0], true); |
258 | 1 | } |
259 | 10 | if (request.params[1].isArray()) { |
260 | 1 | EnableOrDisableLogCategories(request.params[1], false); |
261 | 1 | } |
262 | 10 | BCLog::CategoryMask updated_log_categories = LogInstance().GetCategoryMask(); |
263 | 10 | BCLog::CategoryMask changed_log_categories = original_log_categories ^ updated_log_categories; |
264 | | |
265 | | // Update libevent logging if BCLog::LIBEVENT has changed. |
266 | 10 | if (changed_log_categories & BCLog::LIBEVENT) { |
267 | 0 | UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); |
268 | 0 | } |
269 | | |
270 | 10 | UniValue result(UniValue::VOBJ); |
271 | 310 | for (const auto& logCatActive : LogInstance().LogCategoriesList()) { |
272 | 310 | result.pushKV(logCatActive.category, logCatActive.active); |
273 | 310 | } |
274 | | |
275 | 10 | return result; |
276 | 10 | }, |
277 | 2.36k | }; |
278 | 2.36k | } |
279 | | |
280 | | static RPCMethod echo(const std::string& name) |
281 | 4.71k | { |
282 | 4.71k | return RPCMethod{ |
283 | 4.71k | name, |
284 | 4.71k | "Simply echo back the input arguments. This command is for testing.\n" |
285 | 4.71k | "\nIt will return an internal bug report when arg9='trigger_internal_bug' is passed.\n" |
286 | 4.71k | "\nThe difference between echo and echojson is that echojson has argument conversion enabled in the client-side table in " |
287 | 4.71k | "bitcoin-cli and the GUI. There is no server-side difference.", |
288 | 4.71k | { |
289 | 4.71k | {"arg0", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
290 | 4.71k | {"arg1", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
291 | 4.71k | {"arg2", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
292 | 4.71k | {"arg3", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
293 | 4.71k | {"arg4", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
294 | 4.71k | {"arg5", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
295 | 4.71k | {"arg6", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
296 | 4.71k | {"arg7", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
297 | 4.71k | {"arg8", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
298 | 4.71k | {"arg9", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
299 | 4.71k | }, |
300 | 4.71k | RPCResult{RPCResult::Type::ANY, "", "Returns whatever was passed in"}, |
301 | 4.71k | RPCExamples{""}, |
302 | 4.71k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
303 | 4.71k | { |
304 | 22 | if (request.params[9].isStr()) { |
305 | 0 | CHECK_NONFATAL(request.params[9].get_str() != "trigger_internal_bug"); |
306 | 0 | } |
307 | | |
308 | 22 | return request.params; |
309 | 22 | }, |
310 | 4.71k | }; |
311 | 4.71k | } |
312 | | |
313 | 2.35k | static RPCMethod echo() { return echo("echo"); } |
314 | 2.35k | static RPCMethod echojson() { return echo("echojson"); } |
315 | | |
316 | | static RPCMethod echoipc() |
317 | 2.34k | { |
318 | 2.34k | return RPCMethod{ |
319 | 2.34k | "echoipc", |
320 | 2.34k | "Echo back the input argument, passing it through a spawned process in a multiprocess build.\n" |
321 | 2.34k | "This command is for testing.\n", |
322 | 2.34k | {{"arg", RPCArg::Type::STR, RPCArg::Optional::NO, "The string to echo",}}, |
323 | 2.34k | RPCResult{RPCResult::Type::STR, "echo", "The echoed string."}, |
324 | 2.34k | RPCExamples{HelpExampleCli("echo", "\"Hello world\"") + |
325 | 2.34k | HelpExampleRpc("echo", "\"Hello world\"")}, |
326 | 2.34k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue { |
327 | 1 | interfaces::Init& local_init = *EnsureAnyNodeContext(request.context).init; |
328 | 1 | std::unique_ptr<interfaces::Echo> echo; |
329 | 1 | if (interfaces::Ipc* ipc = local_init.ipc()) { |
330 | | // Spawn a new bitcoin-node process and call makeEcho to get a |
331 | | // client pointer to a interfaces::Echo instance running in |
332 | | // that process. This is just for testing. A slightly more |
333 | | // realistic test spawning a different executable instead of |
334 | | // the same executable would add a new bitcoin-echo executable, |
335 | | // and spawn bitcoin-echo below instead of bitcoin-node. But |
336 | | // using bitcoin-node avoids the need to build and install a |
337 | | // new executable just for this one test. |
338 | 0 | auto init = ipc->spawnProcess("bitcoin-node"); |
339 | 0 | echo = init->makeEcho(); |
340 | 0 | ipc->addCleanup(*echo, [init = init.release()] { delete init; }); |
341 | 1 | } else { |
342 | | // IPC support is not available because this is a bitcoind |
343 | | // process not a bitcoind-node process, so just create a local |
344 | | // interfaces::Echo object and return it so the `echoipc` RPC |
345 | | // method will work, and the python test calling `echoipc` |
346 | | // can expect the same result. |
347 | 1 | echo = local_init.makeEcho(); |
348 | 1 | } |
349 | 1 | return echo->echo(request.params[0].get_str()); |
350 | 1 | }, |
351 | 2.34k | }; |
352 | 2.34k | } |
353 | | |
354 | | static UniValue SummaryToJSON(const IndexSummary&& summary, std::string index_name) |
355 | 169 | { |
356 | 169 | UniValue ret_summary(UniValue::VOBJ); |
357 | 169 | if (!index_name.empty() && index_name != summary.name) return ret_summary; |
358 | | |
359 | 153 | UniValue entry(UniValue::VOBJ); |
360 | 153 | entry.pushKV("synced", summary.synced); |
361 | 153 | entry.pushKV("best_block_height", summary.best_block_height); |
362 | 153 | ret_summary.pushKV(summary.name, std::move(entry)); |
363 | 153 | return ret_summary; |
364 | 169 | } |
365 | | |
366 | | static RPCMethod getindexinfo() |
367 | 2.42k | { |
368 | 2.42k | return RPCMethod{ |
369 | 2.42k | "getindexinfo", |
370 | 2.42k | "Returns the status of one or all available indices currently running in the node.\n", |
371 | 2.42k | { |
372 | 2.42k | {"index_name", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Filter results for an index with a specific name."}, |
373 | 2.42k | }, |
374 | 2.42k | RPCResult{ |
375 | 2.42k | RPCResult::Type::OBJ_DYN, "", "", { |
376 | 2.42k | { |
377 | 2.42k | RPCResult::Type::OBJ, "name", "The name of the index", |
378 | 2.42k | { |
379 | 2.42k | {RPCResult::Type::BOOL, "synced", "Whether the index is synced or not"}, |
380 | 2.42k | {RPCResult::Type::NUM, "best_block_height", "The block height to which the index is synced"}, |
381 | 2.42k | } |
382 | 2.42k | }, |
383 | 2.42k | }, |
384 | 2.42k | }, |
385 | 2.42k | RPCExamples{ |
386 | 2.42k | HelpExampleCli("getindexinfo", "") |
387 | 2.42k | + HelpExampleRpc("getindexinfo", "") |
388 | 2.42k | + HelpExampleCli("getindexinfo", "txindex") |
389 | 2.42k | + HelpExampleRpc("getindexinfo", "txindex") |
390 | 2.42k | }, |
391 | 2.42k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
392 | 2.42k | { |
393 | 66 | UniValue result(UniValue::VOBJ); |
394 | 66 | const std::string index_name{self.MaybeArg<std::string_view>("index_name").value_or("")}; |
395 | | |
396 | 66 | if (g_txindex) { |
397 | 38 | result.pushKVs(SummaryToJSON(g_txindex->GetSummary(), index_name)); |
398 | 38 | } |
399 | | |
400 | 66 | if (g_coin_stats_index) { |
401 | 58 | result.pushKVs(SummaryToJSON(g_coin_stats_index->GetSummary(), index_name)); |
402 | 58 | } |
403 | | |
404 | 66 | if (g_txospenderindex) { |
405 | 28 | result.pushKVs(SummaryToJSON(g_txospenderindex->GetSummary(), index_name)); |
406 | 28 | } |
407 | | |
408 | 66 | ForEachBlockFilterIndex([&result, &index_name](const BlockFilterIndex& index) { |
409 | 45 | result.pushKVs(SummaryToJSON(index.GetSummary(), index_name)); |
410 | 45 | }); |
411 | | |
412 | 66 | return result; |
413 | 66 | }, |
414 | 2.42k | }; |
415 | 2.42k | } |
416 | | |
417 | | void RegisterNodeRPCCommands(CRPCTable& t) |
418 | 1.28k | { |
419 | 1.28k | static const CRPCCommand commands[]{ |
420 | 1.28k | {"control", &getmemoryinfo}, |
421 | 1.28k | {"control", &logging}, |
422 | 1.28k | {"util", &getindexinfo}, |
423 | 1.28k | {"hidden", &setmocktime}, |
424 | 1.28k | {"hidden", &mockscheduler}, |
425 | 1.28k | {"hidden", &echo}, |
426 | 1.28k | {"hidden", &echojson}, |
427 | 1.28k | {"hidden", &echoipc}, |
428 | 1.28k | }; |
429 | 10.2k | for (const auto& c : commands) { |
430 | 10.2k | t.appendCommand(c.name, &c); |
431 | 10.2k | } |
432 | 1.28k | } |