Bitcoin ABC 0.34.2
P2P Digital Currency
rest.cpp
Go to the documentation of this file.
1// Copyright (c) 2009-2010 Satoshi Nakamoto
2// Copyright (c) 2009-2016 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 <rest.h>
7
8#include <blockfilter.h>
9#include <chain.h>
10#include <chainparams.h>
11#include <config.h>
12#include <core_io.h>
13#include <httpserver.h>
15#include <index/txindex.h>
16#include <node/blockstorage.h>
17#include <node/context.h>
18#include <primitives/block.h>
20#include <rpc/blockchain.h>
21#include <rpc/mempool.h>
22#include <rpc/protocol.h>
23#include <rpc/server.h>
24#include <rpc/server_util.h>
25#include <streams.h>
26#include <sync.h>
27#include <txmempool.h>
28#include <util/any.h>
29#include <validation.h>
30
31#include <univalue.h>
32
33#include <any>
34#include <string>
35
39
40// Allow a max of 15 outpoints to be queried at once.
41static const size_t MAX_GETUTXOS_OUTPOINTS = 15;
42static constexpr unsigned int MAX_REST_HEADERS_RESULTS = 2000;
43
44static const struct {
46 const char *name;
47} rf_names[] = {
52};
53
54struct CCoin {
55 uint32_t nHeight{0};
57
58 CCoin() = default;
59 explicit CCoin(Coin in)
60 : nHeight(in.GetHeight()), out(std::move(in.GetTxOut())) {}
61
63 uint32_t nTxVerDummy = 0;
64 READWRITE(nTxVerDummy, obj.nHeight, obj.out);
65 }
66};
67
68static bool RESTERR(HTTPRequest *req, enum HTTPStatusCode status,
69 std::string message) {
70 req->WriteHeader("Content-Type", "text/plain");
71 req->WriteReply(status, message + "\r\n");
72 return false;
73}
74
82static NodeContext *GetNodeContext(const std::any &context, HTTPRequest *req) {
83 auto node_context = util::AnyPtr<NodeContext>(context);
84 if (!node_context) {
86 strprintf("%s:%d (%s)\n"
87 "Internal bug detected: Node context not found!\n"
88 "You may report this issue here: %s\n",
89 __FILE__, __LINE__, __func__, PACKAGE_BUGREPORT));
90 return nullptr;
91 }
92 return node_context;
93}
94
102static CTxMemPool *GetMemPool(const std::any &context, HTTPRequest *req) {
103 auto node_context = util::AnyPtr<NodeContext>(context);
104 if (!node_context || !node_context->mempool) {
105 RESTERR(req, HTTP_NOT_FOUND, "Mempool disabled or instance not found");
106 return nullptr;
107 }
108 return node_context->mempool.get();
109}
110
118static ChainstateManager *GetChainman(const std::any &context,
119 HTTPRequest *req) {
120 auto node_context = util::AnyPtr<NodeContext>(context);
121 if (!node_context || !node_context->chainman) {
123 strprintf("%s:%d (%s)\n"
124 "Internal bug detected: Chainman disabled or instance"
125 " not found!\n"
126 "You may report this issue here: %s\n",
127 __FILE__, __LINE__, __func__, PACKAGE_BUGREPORT));
128 return nullptr;
129 }
130 return node_context->chainman.get();
131}
132
134 const std::string &strReq) {
135 // Remove query string (if any, separated with '?') as it should not
136 // interfere with parsing param and data format
137 param = strReq.substr(0, strReq.rfind('?'));
138 const std::string::size_type pos_format{param.rfind('.')};
139
140 // No format string is found
141 if (pos_format == std::string::npos) {
142 return rf_names[0].rf;
143 }
144
145 // Match format string to available formats
146 const std::string suffix(param, pos_format + 1);
147 for (const auto &rf_name : rf_names) {
148 if (suffix == rf_name.name) {
149 param.erase(pos_format);
150 return rf_name.rf;
151 }
152 }
153
154 // If no suffix is found, return RESTResponseFormat::UNDEF and original
155 // string without query string
156 return rf_names[0].rf;
157}
158
159static std::string AvailableDataFormatsString() {
160 std::string formats;
161 for (const auto &rf_name : rf_names) {
162 if (strlen(rf_name.name) > 0) {
163 formats.append(".");
164 formats.append(rf_name.name);
165 formats.append(", ");
166 }
167 }
168
169 if (formats.length() > 0) {
170 return formats.substr(0, formats.length() - 2);
171 }
172
173 return formats;
174}
175
176static bool CheckWarmup(HTTPRequest *req) {
177 std::string statusmessage;
178 if (RPCIsInWarmup(&statusmessage)) {
180 "Service temporarily unavailable: " + statusmessage);
181 }
182
183 return true;
184}
185
186static bool rest_headers(Config &config, const std::any &context,
187 HTTPRequest *req, const std::string &strURIPart) {
188 if (!CheckWarmup(req)) {
189 return false;
190 }
191
192 std::string param;
193 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
194 std::vector<std::string> path = SplitString(param, '/');
195
196 std::string raw_count;
197 std::string hashStr;
198 if (path.size() == 2) {
199 // deprecated path: /rest/headers/<count>/<hash>
200 hashStr = path[1];
201 raw_count = path[0];
202 } else if (path.size() == 1) {
203 // new path with query parameter: /rest/headers/<hash>?count=<count>
204 hashStr = path[0];
205 try {
206 raw_count = req->GetQueryParameter("count").value_or("5");
207 } catch (const std::runtime_error &e) {
208 return RESTERR(req, HTTP_BAD_REQUEST, e.what());
209 }
210 } else {
211 return RESTERR(req, HTTP_BAD_REQUEST,
212 "Invalid URI format. Expected "
213 "/rest/headers/<hash>.<ext>?count=<count>");
214 }
215
216 const auto parsed_count{ToIntegral<size_t>(raw_count)};
217 if (!parsed_count.has_value() || *parsed_count < 1 ||
218 *parsed_count > MAX_REST_HEADERS_RESULTS) {
219 return RESTERR(
220 req, HTTP_BAD_REQUEST,
221 strprintf(
222 "Header count is invalid or out of acceptable range (1-%u): %s",
223 MAX_REST_HEADERS_RESULTS, raw_count));
224 }
225
226 auto hash{BlockHash::FromHex(hashStr)};
227 if (!hash) {
228 return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
229 }
230
231 const CBlockIndex *tip = nullptr;
232 std::vector<const CBlockIndex *> headers;
233 headers.reserve(*parsed_count);
234 {
235 ChainstateManager *maybe_chainman = GetChainman(context, req);
236 if (!maybe_chainman) {
237 return false;
238 }
239 ChainstateManager &chainman = *maybe_chainman;
240 LOCK(cs_main);
241 CChain &active_chain = chainman.ActiveChain();
242 tip = active_chain.Tip();
243 const CBlockIndex *pindex{chainman.m_blockman.LookupBlockIndex(*hash)};
244 while (pindex != nullptr && active_chain.Contains(pindex)) {
245 headers.push_back(pindex);
246 if (headers.size() == *parsed_count) {
247 break;
248 }
249 pindex = active_chain.Next(pindex);
250 }
251 }
252
253 switch (rf) {
255 DataStream ssHeader{};
256 for (const CBlockIndex *pindex : headers) {
257 ssHeader << pindex->GetBlockHeader();
258 }
259
260 std::string binaryHeader = ssHeader.str();
261 req->WriteHeader("Content-Type", "application/octet-stream");
262 req->WriteReply(HTTP_OK, binaryHeader);
263 return true;
264 }
265
267 DataStream ssHeader{};
268 for (const CBlockIndex *pindex : headers) {
269 ssHeader << pindex->GetBlockHeader();
270 }
271
272 std::string strHex = HexStr(ssHeader) + "\n";
273 req->WriteHeader("Content-Type", "text/plain");
274 req->WriteReply(HTTP_OK, strHex);
275 return true;
276 }
278 UniValue jsonHeaders(UniValue::VARR);
279 for (const CBlockIndex *pindex : headers) {
280 jsonHeaders.push_back(blockheaderToJSON(*tip, *pindex));
281 }
282 std::string strJSON = jsonHeaders.write() + "\n";
283 req->WriteHeader("Content-Type", "application/json");
284 req->WriteReply(HTTP_OK, strJSON);
285 return true;
286 }
287 default: {
288 return RESTERR(req, HTTP_NOT_FOUND,
289 "output format not found (available: " +
291 }
292 }
293}
294
295static bool rest_block(const Config &config, const std::any &context,
296 HTTPRequest *req, const std::string &strURIPart,
297 TxVerbosity tx_verbosity) {
298 if (!CheckWarmup(req)) {
299 return false;
300 }
301
302 std::string hashStr;
303 const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
304
305 auto hash{BlockHash::FromHex(hashStr)};
306 if (!hash) {
307 return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
308 }
309
310 CBlock block;
311 const CBlockIndex *pblockindex = nullptr;
312 const CBlockIndex *tip = nullptr;
313 ChainstateManager *maybe_chainman = GetChainman(context, req);
314 if (!maybe_chainman) {
315 return false;
316 }
317 ChainstateManager &chainman = *maybe_chainman;
318 {
319 LOCK(cs_main);
320 tip = chainman.ActiveTip();
321 pblockindex = chainman.m_blockman.LookupBlockIndex(*hash);
322 if (!pblockindex) {
323 return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
324 }
325 if (chainman.m_blockman.IsBlockPruned(*pblockindex)) {
326 return RESTERR(req, HTTP_NOT_FOUND,
327 hashStr + " not available (pruned data)");
328 }
329 }
330 if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) {
331 return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
332 }
333
334 switch (rf) {
336 DataStream ssBlock{};
337 ssBlock << block;
338 std::string binaryBlock = ssBlock.str();
339 req->WriteHeader("Content-Type", "application/octet-stream");
340 req->WriteReply(HTTP_OK, binaryBlock);
341 return true;
342 }
343
345 DataStream ssBlock{};
346 ssBlock << block;
347 std::string strHex = HexStr(ssBlock) + "\n";
348 req->WriteHeader("Content-Type", "text/plain");
349 req->WriteReply(HTTP_OK, strHex);
350 return true;
351 }
352
354 UniValue objBlock = blockToJSON(chainman.m_blockman, block, *tip,
355 *pblockindex, tx_verbosity);
356 std::string strJSON = objBlock.write() + "\n";
357 req->WriteHeader("Content-Type", "application/json");
358 req->WriteReply(HTTP_OK, strJSON);
359 return true;
360 }
361
362 default: {
363 return RESTERR(req, HTTP_NOT_FOUND,
364 "output format not found (available: " +
366 }
367 }
368}
369
370static bool rest_block_extended(Config &config, const std::any &context,
371 HTTPRequest *req,
372 const std::string &strURIPart) {
373 return rest_block(config, context, req, strURIPart,
375}
376
377static bool rest_block_notxdetails(Config &config, const std::any &context,
378 HTTPRequest *req,
379 const std::string &strURIPart) {
380 return rest_block(config, context, req, strURIPart, TxVerbosity::SHOW_TXID);
381}
382
383static bool rest_filter_header(Config &config, const std::any &context,
384 HTTPRequest *req,
385 const std::string &strURIPart) {
386 if (!CheckWarmup(req)) {
387 return false;
388 }
389
390 std::string param;
391 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
392
393 std::vector<std::string> uri_parts = SplitString(param, '/');
394 std::string raw_count;
395 std::string raw_blockhash;
396 if (uri_parts.size() == 3) {
397 // deprecated path:
398 // /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>
399 raw_blockhash = uri_parts[2];
400 raw_count = uri_parts[1];
401 } else if (uri_parts.size() == 2) {
402 // new path with query parameter:
403 // /rest/blockfilterheaders/<filtertype>/<blockhash>?count=<count>
404 raw_blockhash = uri_parts[1];
405 try {
406 raw_count = req->GetQueryParameter("count").value_or("5");
407 } catch (const std::runtime_error &e) {
408 return RESTERR(req, HTTP_BAD_REQUEST, e.what());
409 }
410 } else {
411 return RESTERR(req, HTTP_BAD_REQUEST,
412 "Invalid URI format. Expected "
413 "/rest/blockfilterheaders/<filtertype>/"
414 "<blockhash>.<ext>?count=<count>");
415 }
416
417 const auto parsed_count{ToIntegral<size_t>(raw_count)};
418 if (!parsed_count.has_value() || *parsed_count < 1 ||
419 *parsed_count > MAX_REST_HEADERS_RESULTS) {
420 return RESTERR(
421 req, HTTP_BAD_REQUEST,
422 strprintf(
423 "Header count is invalid or out of acceptable range (1-%u): %s",
424 MAX_REST_HEADERS_RESULTS, raw_count));
425 }
426
427 auto block_hash{BlockHash::FromHex(raw_blockhash)};
428 if (!block_hash) {
429 return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + raw_blockhash);
430 }
431
432 BlockFilterType filtertype;
433 if (!BlockFilterTypeByName(uri_parts[0], filtertype)) {
434 return RESTERR(req, HTTP_BAD_REQUEST,
435 "Unknown filtertype " + uri_parts[0]);
436 }
437
438 BlockFilterIndex *index = GetBlockFilterIndex(filtertype);
439 if (!index) {
440 return RESTERR(req, HTTP_BAD_REQUEST,
441 "Index is not enabled for filtertype " + uri_parts[0]);
442 }
443
444 std::vector<const CBlockIndex *> headers;
445 headers.reserve(*parsed_count);
446 {
447 ChainstateManager *maybe_chainman = GetChainman(context, req);
448 if (!maybe_chainman) {
449 return false;
450 }
451 ChainstateManager &chainman = *maybe_chainman;
452 LOCK(cs_main);
453 CChain &active_chain = chainman.ActiveChain();
454 const CBlockIndex *pindex{
455 chainman.m_blockman.LookupBlockIndex(*block_hash)};
456 while (pindex != nullptr && active_chain.Contains(pindex)) {
457 headers.push_back(pindex);
458 if (headers.size() == *parsed_count) {
459 break;
460 }
461 pindex = active_chain.Next(pindex);
462 }
463 }
464
465 bool index_ready = index->BlockUntilSyncedToCurrentChain();
466
467 std::vector<uint256> filter_headers;
468 filter_headers.reserve(*parsed_count);
469 for (const CBlockIndex *pindex : headers) {
470 uint256 filter_header;
471 if (!index->LookupFilterHeader(pindex, filter_header)) {
472 std::string errmsg = "Filter not found.";
473
474 if (!index_ready) {
475 errmsg +=
476 " Block filters are still in the process of being indexed.";
477 } else {
478 errmsg +=
479 " This error is unexpected and indicates index corruption.";
480 }
481
482 return RESTERR(req, HTTP_NOT_FOUND, errmsg);
483 }
484 filter_headers.push_back(filter_header);
485 }
486
487 switch (rf) {
489 DataStream ssHeader{};
490 for (const uint256 &header : filter_headers) {
491 ssHeader << header;
492 }
493
494 std::string binaryHeader = ssHeader.str();
495 req->WriteHeader("Content-Type", "application/octet-stream");
496 req->WriteReply(HTTP_OK, binaryHeader);
497 return true;
498 }
500 DataStream ssHeader{};
501 for (const uint256 &header : filter_headers) {
502 ssHeader << header;
503 }
504
505 std::string strHex = HexStr(ssHeader) + "\n";
506 req->WriteHeader("Content-Type", "text/plain");
507 req->WriteReply(HTTP_OK, strHex);
508 return true;
509 }
511 UniValue jsonHeaders(UniValue::VARR);
512 for (const uint256 &header : filter_headers) {
513 jsonHeaders.push_back(header.GetHex());
514 }
515
516 std::string strJSON = jsonHeaders.write() + "\n";
517 req->WriteHeader("Content-Type", "application/json");
518 req->WriteReply(HTTP_OK, strJSON);
519 return true;
520 }
521 default: {
522 return RESTERR(req, HTTP_NOT_FOUND,
523 "output format not found (available: " +
525 }
526 }
527}
528
529static bool rest_block_filter(Config &config, const std::any &context,
530 HTTPRequest *req, const std::string &strURIPart) {
531 if (!CheckWarmup(req)) {
532 return false;
533 }
534
535 std::string param;
536 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
537
538 // request is sent over URI scheme /rest/blockfilter/filtertype/blockhash
539 std::vector<std::string> uri_parts = SplitString(param, '/');
540 if (uri_parts.size() != 2) {
541 return RESTERR(req, HTTP_BAD_REQUEST,
542 "Invalid URI format. Expected "
543 "/rest/blockfilter/<filtertype>/<blockhash>");
544 }
545
546 auto block_hash{BlockHash::FromHex(uri_parts[1])};
547 if (!block_hash) {
548 return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[1]);
549 }
550
551 BlockFilterType filtertype;
552 if (!BlockFilterTypeByName(uri_parts[0], filtertype)) {
553 return RESTERR(req, HTTP_BAD_REQUEST,
554 "Unknown filtertype " + uri_parts[0]);
555 }
556
557 BlockFilterIndex *index = GetBlockFilterIndex(filtertype);
558 if (!index) {
559 return RESTERR(req, HTTP_BAD_REQUEST,
560 "Index is not enabled for filtertype " + uri_parts[0]);
561 }
562
563 const CBlockIndex *block_index;
564 bool block_was_connected;
565 {
566 ChainstateManager *maybe_chainman = GetChainman(context, req);
567 if (!maybe_chainman) {
568 return false;
569 }
570 ChainstateManager &chainman = *maybe_chainman;
571 LOCK(cs_main);
572 block_index = chainman.m_blockman.LookupBlockIndex(*block_hash);
573 if (!block_index) {
574 return RESTERR(req, HTTP_NOT_FOUND, uri_parts[1] + " not found");
575 }
576 block_was_connected = block_index->IsValid(BlockValidity::SCRIPTS);
577 }
578
579 bool index_ready = index->BlockUntilSyncedToCurrentChain();
580
581 BlockFilter filter;
582 if (!index->LookupFilter(block_index, filter)) {
583 std::string errmsg = "Filter not found.";
584
585 if (!block_was_connected) {
586 errmsg += " Block was not connected to active chain.";
587 } else if (!index_ready) {
588 errmsg +=
589 " Block filters are still in the process of being indexed.";
590 } else {
591 errmsg +=
592 " This error is unexpected and indicates index corruption.";
593 }
594
595 return RESTERR(req, HTTP_NOT_FOUND, errmsg);
596 }
597
598 switch (rf) {
600 DataStream ssResp{};
601 ssResp << filter;
602
603 std::string binaryResp = ssResp.str();
604 req->WriteHeader("Content-Type", "application/octet-stream");
605 req->WriteReply(HTTP_OK, binaryResp);
606 return true;
607 }
609 DataStream ssResp{};
610 ssResp << filter;
611
612 std::string strHex = HexStr(ssResp) + "\n";
613 req->WriteHeader("Content-Type", "text/plain");
614 req->WriteReply(HTTP_OK, strHex);
615 return true;
616 }
619 ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
620 std::string strJSON = ret.write() + "\n";
621 req->WriteHeader("Content-Type", "application/json");
622 req->WriteReply(HTTP_OK, strJSON);
623 return true;
624 }
625 default: {
626 return RESTERR(req, HTTP_NOT_FOUND,
627 "output format not found (available: " +
629 }
630 }
631}
632
633static bool rest_chaininfo(Config &config, const std::any &context,
634 HTTPRequest *req, const std::string &strURIPart) {
635 if (!CheckWarmup(req)) {
636 return false;
637 }
638
639 std::string param;
640 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
641
642 switch (rf) {
644 JSONRPCRequest jsonRequest;
645 jsonRequest.context = context;
646 jsonRequest.params = UniValue(UniValue::VARR);
647 UniValue chainInfoObject =
648 getblockchaininfo().HandleRequest(config, jsonRequest);
649 std::string strJSON = chainInfoObject.write() + "\n";
650 req->WriteHeader("Content-Type", "application/json");
651 req->WriteReply(HTTP_OK, strJSON);
652 return true;
653 }
654 default: {
655 return RESTERR(req, HTTP_NOT_FOUND,
656 "output format not found (available: json)");
657 }
658 }
659}
660
661static bool rest_mempool_info(Config &config, const std::any &context,
662 HTTPRequest *req, const std::string &strURIPart) {
663 if (!CheckWarmup(req)) {
664 return false;
665 }
666
667 const CTxMemPool *mempool = GetMemPool(context, req);
668 if (!mempool) {
669 return false;
670 }
671
672 std::string param;
673 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
674
675 switch (rf) {
677 UniValue mempoolInfoObject = MempoolInfoToJSON(*mempool);
678
679 std::string strJSON = mempoolInfoObject.write() + "\n";
680 req->WriteHeader("Content-Type", "application/json");
681 req->WriteReply(HTTP_OK, strJSON);
682 return true;
683 }
684 default: {
685 return RESTERR(req, HTTP_NOT_FOUND,
686 "output format not found (available: json)");
687 }
688 }
689}
690
691static bool rest_mempool_contents(Config &config, const std::any &context,
692 HTTPRequest *req,
693 const std::string &strURIPart) {
694 // TODO: when backporting core#25760 & core#26207, apply bugfix core#27468
695 if (!CheckWarmup(req)) {
696 return false;
697 }
698
699 const CTxMemPool *mempool = GetMemPool(context, req);
700 if (!mempool) {
701 return false;
702 }
703
704 std::string param;
705 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
706
707 switch (rf) {
709 UniValue mempoolObject = MempoolToJSON(*mempool, true);
710
711 std::string strJSON = mempoolObject.write() + "\n";
712 req->WriteHeader("Content-Type", "application/json");
713 req->WriteReply(HTTP_OK, strJSON);
714 return true;
715 }
716 default: {
717 return RESTERR(req, HTTP_NOT_FOUND,
718 "output format not found (available: json)");
719 }
720 }
721}
722
723static bool rest_tx(Config &config, const std::any &context, HTTPRequest *req,
724 const std::string &strURIPart) {
725 if (!CheckWarmup(req)) {
726 return false;
727 }
728
729 std::string hashStr;
730 const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
731
732 auto txid{TxId::FromHex(hashStr)};
733 if (!txid) {
734 return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
735 }
736 if (g_txindex) {
737 g_txindex->BlockUntilSyncedToCurrentChain();
738 }
739
740 const NodeContext *const node = GetNodeContext(context, req);
741 if (!node) {
742 return false;
743 }
744 BlockHash hashBlock;
745 const CTransactionRef tx =
746 GetTransaction(/* block_index */ nullptr, node->mempool.get(), *txid,
747 hashBlock, node->chainman->m_blockman);
748 if (!tx) {
749 return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
750 }
751
752 switch (rf) {
754 DataStream ssTx{};
755 ssTx << tx;
756
757 std::string binaryTx = ssTx.str();
758 req->WriteHeader("Content-Type", "application/octet-stream");
759 req->WriteReply(HTTP_OK, binaryTx);
760 return true;
761 }
762
764 DataStream ssTx{};
765 ssTx << tx;
766
767 std::string strHex = HexStr(ssTx) + "\n";
768 req->WriteHeader("Content-Type", "text/plain");
769 req->WriteReply(HTTP_OK, strHex);
770 return true;
771 }
772
775 TxToUniv(*tx, hashBlock, objTx);
776 std::string strJSON = objTx.write() + "\n";
777 req->WriteHeader("Content-Type", "application/json");
778 req->WriteReply(HTTP_OK, strJSON);
779 return true;
780 }
781
782 default: {
783 return RESTERR(req, HTTP_NOT_FOUND,
784 "output format not found (available: " +
786 }
787 }
788}
789
790static bool rest_getutxos(Config &config, const std::any &context,
791 HTTPRequest *req, const std::string &strURIPart) {
792 if (!CheckWarmup(req)) {
793 return false;
794 }
795
796 std::string param;
797 const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
798
799 std::vector<std::string> uriParts;
800 if (param.length() > 1) {
801 std::string strUriParams = param.substr(1);
802 uriParts = SplitString(strUriParams, '/');
803 }
804
805 // throw exception in case of an empty request
806 std::string strRequestMutable = req->ReadBody();
807 if (strRequestMutable.length() == 0 && uriParts.size() == 0) {
808 return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
809 }
810
811 bool fInputParsed = false;
812 bool fCheckMemPool = false;
813 std::vector<COutPoint> vOutPoints;
814
815 // parse/deserialize input
816 // input-format = output-format, rest/getutxos/bin requires binary input,
817 // gives binary output, ...
818
819 if (uriParts.size() > 0) {
820 // inputs is sent over URI scheme
821 // (/rest/getutxos/checkmempool/txid1-n/txid2-n/...)
822 if (uriParts[0] == "checkmempool") {
823 fCheckMemPool = true;
824 }
825
826 for (size_t i = (fCheckMemPool) ? 1 : 0; i < uriParts.size(); i++) {
827 const auto txid_out{
828 util::Split<std::string_view>(uriParts[i], '-')};
829 if (txid_out.size() != 2) {
830 return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
831 }
832 auto txid{TxId::FromHex(txid_out.at(0))};
833 auto output{ToIntegral<uint32_t>(txid_out.at(1))};
834
835 if (!txid || !output) {
836 return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
837 }
838
839 vOutPoints.push_back(COutPoint(*txid, *output));
840 }
841
842 if (vOutPoints.size() > 0) {
843 fInputParsed = true;
844 } else {
845 return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
846 }
847 }
848
849 switch (rf) {
851 // convert hex to bin, continue then with bin part
852 std::vector<uint8_t> strRequestV = ParseHex(strRequestMutable);
853 strRequestMutable.assign(strRequestV.begin(), strRequestV.end());
854 }
855 // FALLTHROUGH
857 try {
858 // deserialize only if user sent a request
859 if (strRequestMutable.size() > 0) {
860 // don't allow sending input over URI and HTTP RAW DATA
861 if (fInputParsed) {
862 return RESTERR(req, HTTP_BAD_REQUEST,
863 "Combination of URI scheme inputs and "
864 "raw post data is not allowed");
865 }
866
867 DataStream oss{};
868 oss << strRequestMutable;
869 oss >> fCheckMemPool;
870 oss >> vOutPoints;
871 }
872 } catch (const std::ios_base::failure &) {
873 // abort in case of unreadable binary data
874 return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
875 }
876 break;
877 }
878
880 if (!fInputParsed) {
881 return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
882 }
883 break;
884 }
885 default: {
886 return RESTERR(req, HTTP_NOT_FOUND,
887 "output format not found (available: " +
889 }
890 }
891
892 // limit max outpoints
893 if (vOutPoints.size() > MAX_GETUTXOS_OUTPOINTS) {
894 return RESTERR(
895 req, HTTP_BAD_REQUEST,
896 strprintf("Error: max outpoints exceeded (max: %d, tried: %d)",
897 MAX_GETUTXOS_OUTPOINTS, vOutPoints.size()));
898 }
899
900 // check spentness and form a bitmap (as well as a JSON capable
901 // human-readable string representation)
902 std::vector<uint8_t> bitmap;
903 std::vector<CCoin> outs;
904 std::string bitmapStringRepresentation;
905 std::vector<bool> hits;
906 bitmap.resize((vOutPoints.size() + 7) / 8);
907 ChainstateManager *maybe_chainman = GetChainman(context, req);
908 if (!maybe_chainman) {
909 return false;
910 }
911 ChainstateManager &chainman = *maybe_chainman;
912 decltype(chainman.ActiveHeight()) active_height;
913 BlockHash active_hash;
914 {
915 auto process_utxos =
916 [&vOutPoints, &outs, &hits, &active_height, &active_hash,
917 &chainman](const CCoinsView &view, const CTxMemPool *mempool)
919 for (const COutPoint &vOutPoint : vOutPoints) {
920 auto coin = !mempool || !mempool->isSpent(vOutPoint)
921 ? view.GetCoin(vOutPoint)
922 : std::nullopt;
923 hits.push_back(coin.has_value());
924 if (coin) {
925 outs.emplace_back(std::move(*coin));
926 }
927 }
928 active_height = chainman.ActiveHeight();
929 active_hash = chainman.ActiveTip()->GetBlockHash();
930 };
931
932 if (fCheckMemPool) {
933 const CTxMemPool *mempool = GetMemPool(context, req);
934 if (!mempool) {
935 return false;
936 }
937
938 // use db+mempool as cache backend in case user likes to query
939 // mempool
940 LOCK2(cs_main, mempool->cs);
941 CCoinsViewCache &viewChain = chainman.ActiveChainstate().CoinsTip();
942 CCoinsViewMemPool viewMempool(&viewChain, *mempool);
943 process_utxos(viewMempool, mempool);
944 } else {
945 // no need to lock mempool!
946 LOCK(cs_main);
947 process_utxos(chainman.ActiveChainstate().CoinsTip(), nullptr);
948 }
949
950 for (size_t i = 0; i < hits.size(); ++i) {
951 const bool hit = hits[i];
952 // form a binary string representation (human-readable for json
953 // output)
954 bitmapStringRepresentation.append(hit ? "1" : "0");
955 bitmap[i / 8] |= ((uint8_t)hit) << (i % 8);
956 }
957 }
958
959 switch (rf) {
961 // serialize data
962 // use exact same output as mentioned in Bip64
963 DataStream ssGetUTXOResponse{};
964 ssGetUTXOResponse << active_height << active_hash << bitmap << outs;
965 std::string ssGetUTXOResponseString = ssGetUTXOResponse.str();
966
967 req->WriteHeader("Content-Type", "application/octet-stream");
968 req->WriteReply(HTTP_OK, ssGetUTXOResponseString);
969 return true;
970 }
971
973 DataStream ssGetUTXOResponse{};
974 ssGetUTXOResponse << active_height << active_hash << bitmap << outs;
975 std::string strHex = HexStr(ssGetUTXOResponse) + "\n";
976
977 req->WriteHeader("Content-Type", "text/plain");
978 req->WriteReply(HTTP_OK, strHex);
979 return true;
980 }
981
983 UniValue objGetUTXOResponse(UniValue::VOBJ);
984
985 // pack in some essentials
986 // use more or less the same output as mentioned in Bip64
987 objGetUTXOResponse.pushKV("chainHeight", active_height);
988 objGetUTXOResponse.pushKV("chaintipHash", active_hash.GetHex());
989 objGetUTXOResponse.pushKV("bitmap", bitmapStringRepresentation);
990
992 for (const CCoin &coin : outs) {
994 utxo.pushKV("height", int32_t(coin.nHeight));
995 utxo.pushKV("value", coin.out.nValue);
996
997 // include the script in a json output
999 ScriptPubKeyToUniv(coin.out.scriptPubKey, o, true);
1000 utxo.pushKV("scriptPubKey", std::move(o));
1001 utxos.push_back(std::move(utxo));
1002 }
1003 objGetUTXOResponse.pushKV("utxos", std::move(utxos));
1004
1005 // return json string
1006 std::string strJSON = objGetUTXOResponse.write() + "\n";
1007 req->WriteHeader("Content-Type", "application/json");
1008 req->WriteReply(HTTP_OK, strJSON);
1009 return true;
1010 }
1011 default: {
1012 return RESTERR(req, HTTP_NOT_FOUND,
1013 "output format not found (available: " +
1015 }
1016 }
1017}
1018
1019static bool rest_blockhash_by_height(Config &config, const std::any &context,
1020 HTTPRequest *req,
1021 const std::string &str_uri_part) {
1022 if (!CheckWarmup(req)) {
1023 return false;
1024 }
1025 std::string height_str;
1026 const RESTResponseFormat rf = ParseDataFormat(height_str, str_uri_part);
1027
1028 int32_t blockheight;
1029 if (!ParseInt32(height_str, &blockheight) || blockheight < 0) {
1030 return RESTERR(req, HTTP_BAD_REQUEST,
1031 "Invalid height: " + SanitizeString(height_str));
1032 }
1033
1034 CBlockIndex *pblockindex = nullptr;
1035 {
1036 ChainstateManager *maybe_chainman = GetChainman(context, req);
1037 if (!maybe_chainman) {
1038 return false;
1039 }
1040 ChainstateManager &chainman = *maybe_chainman;
1041 LOCK(cs_main);
1042 const CChain &active_chain = chainman.ActiveChain();
1043 if (blockheight > active_chain.Height()) {
1044 return RESTERR(req, HTTP_NOT_FOUND, "Block height out of range");
1045 }
1046 pblockindex = active_chain[blockheight];
1047 }
1048 switch (rf) {
1050 DataStream ss_blockhash{};
1051 ss_blockhash << pblockindex->GetBlockHash();
1052 req->WriteHeader("Content-Type", "application/octet-stream");
1053 req->WriteReply(HTTP_OK, ss_blockhash.str());
1054 return true;
1055 }
1057 req->WriteHeader("Content-Type", "text/plain");
1058 req->WriteReply(HTTP_OK,
1059 pblockindex->GetBlockHash().GetHex() + "\n");
1060 return true;
1061 }
1063 req->WriteHeader("Content-Type", "application/json");
1065 resp.pushKV("blockhash", pblockindex->GetBlockHash().GetHex());
1066 req->WriteReply(HTTP_OK, resp.write() + "\n");
1067 return true;
1068 }
1069 default: {
1070 return RESTERR(req, HTTP_NOT_FOUND,
1071 "output format not found (available: " +
1073 }
1074 }
1075}
1076
1077static const struct {
1078 const char *prefix;
1079 bool (*handler)(Config &config, const std::any &context, HTTPRequest *req,
1080 const std::string &strReq);
1081} uri_prefixes[] = {
1082 {"/rest/tx/", rest_tx},
1083 {"/rest/block/notxdetails/", rest_block_notxdetails},
1084 {"/rest/block/", rest_block_extended},
1085 {"/rest/blockfilter/", rest_block_filter},
1086 {"/rest/blockfilterheaders/", rest_filter_header},
1087 {"/rest/chaininfo", rest_chaininfo},
1088 {"/rest/mempool/info", rest_mempool_info},
1089 {"/rest/mempool/contents", rest_mempool_contents},
1090 {"/rest/headers/", rest_headers},
1091 {"/rest/getutxos", rest_getutxos},
1092 {"/rest/blockhashbyheight/", rest_blockhash_by_height},
1094
1095void StartREST(const std::any &context) {
1096 for (const auto &up : uri_prefixes) {
1097 auto handler = [context, up](Config &config, HTTPRequest *req,
1098 const std::string &prefix) {
1099 return up.handler(config, context, req, prefix);
1100 };
1101 RegisterHTTPHandler(up.prefix, false, handler);
1102 }
1103}
1104
1106
1107void StopREST() {
1108 for (const auto &up : uri_prefixes) {
1109 UnregisterHTTPHandler(up.prefix, false);
1110 }
1111}
RPCHelpMan getblockchaininfo()
UniValue blockheaderToJSON(const CBlockIndex &tip, const CBlockIndex &blockindex)
Block header to JSON.
Definition: blockchain.cpp:147
UniValue blockToJSON(BlockManager &blockman, const CBlock &block, const CBlockIndex &tip, const CBlockIndex &blockindex, TxVerbosity verbosity)
Block description to JSON.
Definition: blockchain.cpp:181
bool BlockFilterTypeByName(const std::string &name, BlockFilterType &filter_type)
Find a filter type by its human-readable name.
BlockFilterType
Definition: blockfilter.h:88
BlockFilterIndex * GetBlockFilterIndex(BlockFilterType filter_type)
Get a block filter index by type.
@ SCRIPTS
Scripts & signatures ok.
Complete block filter struct as defined in BIP 157.
Definition: blockfilter.h:111
const std::vector< uint8_t > & GetEncodedFilter() const
Definition: blockfilter.h:134
BlockFilterIndex is used to store and retrieve block filters, hashes, and headers for a range of bloc...
bool LookupFilter(const CBlockIndex *block_index, BlockFilter &filter_out) const
Get a single filter by block.
bool LookupFilterHeader(const CBlockIndex *block_index, uint256 &header_out) EXCLUSIVE_LOCKS_REQUIRED(!m_cs_headers_cache)
Get a single filter header by block.
Definition: block.h:60
The block chain is a tree shaped structure starting with the genesis block at the root,...
Definition: blockindex.h:25
bool IsValid(enum BlockValidity nUpTo=BlockValidity::TRANSACTIONS) const EXCLUSIVE_LOCKS_REQUIRED(
Check whether this block index entry is valid up to the passed validity level.
Definition: blockindex.h:190
BlockHash GetBlockHash() const
Definition: blockindex.h:129
An in-memory indexed chain of blocks.
Definition: chain.h:138
CBlockIndex * Tip() const
Returns the index entry for the tip of this chain, or nullptr if none.
Definition: chain.h:154
CBlockIndex * Next(const CBlockIndex *pindex) const
Find the successor of a block in this chain, or nullptr if the given index is not found or is the tip...
Definition: chain.h:178
int Height() const
Return the maximal height in the chain.
Definition: chain.h:190
bool Contains(const CBlockIndex *pindex) const
Efficiently check whether a block is present in this chain.
Definition: chain.h:170
CCoinsView that adds a memory cache for transactions to another CCoinsView.
Definition: coins.h:357
Abstract view on the open txout dataset.
Definition: coins.h:303
CCoinsView that brings transactions from a mempool into view.
Definition: txmempool.h:643
CTxMemPool stores valid-according-to-the-current-best-chain transactions that may be included in the ...
Definition: txmempool.h:222
RecursiveMutex cs
This mutex needs to be locked when accessing mapTx or other members that are guarded by it.
Definition: txmempool.h:316
An output of a transaction.
Definition: transaction.h:128
Provides an interface for creating and interacting with one or two chainstates: an IBD chainstate gen...
Definition: validation.h:1176
SnapshotCompletionResult MaybeCompleteSnapshotValidation() EXCLUSIVE_LOCKS_REQUIRED(const CBlockIndex *GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(Chainstate ActiveChainstate)() const
Once the background validation chainstate has reached the height which is the base of the UTXO snapsh...
Definition: validation.h:1446
RecursiveMutex & GetMutex() const LOCK_RETURNED(
Alias for cs_main.
Definition: validation.h:1328
CBlockIndex * ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Definition: validation.h:1453
int ActiveHeight() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Definition: validation.h:1450
CChain & ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Definition: validation.h:1447
node::BlockManager m_blockman
A single BlockManager instance is shared across each constructed chainstate to avoid duplicating bloc...
Definition: validation.h:1336
A UTXO entry.
Definition: coins.h:31
Definition: config.h:19
Double ended buffer combining vector and stream-like interfaces.
Definition: streams.h:118
std::string str() const
Definition: streams.h:140
In-flight HTTP request.
Definition: httpserver.h:72
std::optional< std::string > GetQueryParameter(const std::string &key) const
Get the query parameter value from request uri for a specified key, or std::nullopt if the key is not...
Definition: httpserver.cpp:763
void WriteReply(int nStatus, const std::string &strReply="")
Write HTTP reply.
Definition: httpserver.cpp:697
void WriteHeader(const std::string &hdr, const std::string &value)
Write output header.
Definition: httpserver.cpp:685
std::string ReadBody()
Read request body.
Definition: httpserver.cpp:661
UniValue params
Definition: request.h:34
std::any context
Definition: request.h:39
UniValue HandleRequest(const Config &config, const JSONRPCRequest &request) const
Definition: util.cpp:611
void push_back(UniValue val)
Definition: univalue.cpp:96
@ VOBJ
Definition: univalue.h:31
@ VARR
Definition: univalue.h:32
std::string write(unsigned int prettyIndent=0, unsigned int indentLevel=0) const
void pushKV(std::string key, UniValue val)
Definition: univalue.cpp:115
std::string GetHex() const
Definition: uint256.cpp:10
CBlockIndex * LookupBlockIndex(const BlockHash &hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
bool ReadBlock(CBlock &block, const FlatFilePos &pos) const
Functions for disk access for blocks.
256-bit opaque blob.
Definition: uint256.h:199
void ScriptPubKeyToUniv(const CScript &scriptPubKey, UniValue &out, bool fIncludeHex)
Definition: core_write.cpp:195
TxVerbosity
Verbose level for block's transaction.
Definition: core_io.h:29
@ SHOW_DETAILS_AND_PREVOUT
The same as previous option with information about prevouts if available.
@ SHOW_TXID
Only TXID for each block's transaction.
void TxToUniv(const CTransaction &tx, const BlockHash &hashBlock, UniValue &entry, bool include_hex=true, const CTxUndo *txundo=nullptr, TxVerbosity verbosity=TxVerbosity::SHOW_DETAILS, std::function< bool(const CTxOut &)> is_change_func={})
Definition: core_write.cpp:222
RecursiveMutex cs_main
Mutex to guard access to validation specific variables, such as reading or changing the chainstate.
Definition: cs_main.cpp:7
std::string HexStr(const Span< const uint8_t > s)
Convert a span of bytes to a lower-case hexadecimal string.
Definition: hex_base.cpp:30
void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
Unregister handler for prefix.
Definition: httpserver.cpp:804
void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler)
Register handler for prefix.
Definition: httpserver.cpp:797
UniValue MempoolInfoToJSON(const CTxMemPool &pool)
Mempool information to JSON.
Definition: mempool.cpp:689
UniValue MempoolToJSON(const CTxMemPool &pool, bool verbose, bool include_mempool_sequence)
Mempool to JSON.
Definition: mempool.cpp:403
Definition: messages.h:12
CTransactionRef GetTransaction(const CBlockIndex *const block_index, const CTxMemPool *const mempool, const TxId &txid, BlockHash &hashBlock, const BlockManager &blockman)
Return transaction with a given txid.
Implement std::hash so RCUPtr can be used as a key for maps or sets.
Definition: rcu.h:259
std::vector< std::string > SplitString(std::string_view str, char sep)
Definition: string.h:63
std::shared_ptr< const CTransaction > CTransactionRef
Definition: transaction.h:315
static constexpr unsigned int MAX_REST_HEADERS_RESULTS
Definition: rest.cpp:42
static bool rest_filter_header(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:383
RESTResponseFormat rf
Definition: rest.cpp:45
static bool rest_block_filter(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:529
static bool rest_blockhash_by_height(Config &config, const std::any &context, HTTPRequest *req, const std::string &str_uri_part)
Definition: rest.cpp:1019
const char * prefix
Definition: rest.cpp:1078
void StartREST(const std::any &context)
Start HTTP REST subsystem.
Definition: rest.cpp:1095
bool(* handler)(Config &config, const std::any &context, HTTPRequest *req, const std::string &strReq)
Definition: rest.cpp:1079
static bool rest_block(const Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart, TxVerbosity tx_verbosity)
Definition: rest.cpp:295
void StopREST()
Stop HTTP REST subsystem.
Definition: rest.cpp:1107
static bool rest_mempool_info(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:661
const char * name
Definition: rest.cpp:46
static const struct @12 rf_names[]
static bool rest_headers(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:186
static bool rest_block_extended(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:370
static bool rest_mempool_contents(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:691
void InterruptREST()
Interrupt RPC REST subsystem.
Definition: rest.cpp:1105
static bool RESTERR(HTTPRequest *req, enum HTTPStatusCode status, std::string message)
Definition: rest.cpp:68
static bool CheckWarmup(HTTPRequest *req)
Definition: rest.cpp:176
static bool rest_block_notxdetails(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:377
static ChainstateManager * GetChainman(const std::any &context, HTTPRequest *req)
Get the node context chainstatemanager.
Definition: rest.cpp:118
static bool rest_chaininfo(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:633
static bool rest_tx(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:723
static const struct @13 uri_prefixes[]
RESTResponseFormat ParseDataFormat(std::string &param, const std::string &strReq)
Parse a URI to get the data format and URI without data format and query string.
Definition: rest.cpp:133
static CTxMemPool * GetMemPool(const std::any &context, HTTPRequest *req)
Get the node context mempool.
Definition: rest.cpp:102
static bool rest_getutxos(Config &config, const std::any &context, HTTPRequest *req, const std::string &strURIPart)
Definition: rest.cpp:790
static const size_t MAX_GETUTXOS_OUTPOINTS
Definition: rest.cpp:41
static std::string AvailableDataFormatsString()
Definition: rest.cpp:159
static NodeContext * GetNodeContext(const std::any &context, HTTPRequest *req)
Get the node context.
Definition: rest.cpp:82
RESTResponseFormat
Definition: rest.h:10
HTTPStatusCode
HTTP status codes.
Definition: protocol.h:10
@ HTTP_BAD_REQUEST
Definition: protocol.h:12
@ HTTP_OK
Definition: protocol.h:11
@ HTTP_SERVICE_UNAVAILABLE
Definition: protocol.h:18
@ HTTP_NOT_FOUND
Definition: protocol.h:15
@ HTTP_INTERNAL_SERVER_ERROR
Definition: protocol.h:17
#define READWRITE(...)
Definition: serialize.h:176
bool RPCIsInWarmup(std::string *outStatus)
Returns the current warmup state.
Definition: server.cpp:402
A BlockHash is a unqiue identifier for a block.
Definition: blockhash.h:13
static std::optional< BlockHash > FromHex(std::string_view str)
Definition: blockhash.h:17
Definition: rest.cpp:54
CTxOut out
Definition: rest.cpp:56
CCoin()=default
CCoin(Coin in)
Definition: rest.cpp:59
uint32_t nHeight
Definition: rest.cpp:55
SERIALIZE_METHODS(CCoin, obj)
Definition: rest.cpp:62
static std::optional< TxId > FromHex(std::string_view str)
Definition: txid.h:18
NodeContext struct containing references to chain state and connection state.
Definition: context.h:50
#define LOCK2(cs1, cs2)
Definition: sync.h:298
#define LOCK(cs)
Definition: sync.h:295
#define EXCLUSIVE_LOCKS_REQUIRED(...)
Definition: threadsafety.h:56
#define strprintf
Format arguments and return the string or write to given std::ostream (see tinyformat::format doc for...
Definition: tinyformat.h:1203
std::unique_ptr< TxIndex > g_txindex
The global transaction index, used in GetTransaction. May be null.
Definition: txindex.cpp:17
bool ParseInt32(std::string_view str, int32_t *out)
Convert string to signed 32-bit integer with strict parse error feedback.
template std::vector< std::byte > ParseHex(std::string_view)
std::string SanitizeString(std::string_view str, int rule)
Remove unsafe chars.