diff --git a/src/Makefile.am b/src/Makefile.am index 77fc92ee81..4b38c26378 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -268,6 +268,7 @@ BITCOIN_CORE_H = \ rpc/mempool.h \ rpc/mining.h \ rpc/protocol.h \ + rpc/rawtransaction.h \ rpc/rawtransaction_util.h \ rpc/register.h \ rpc/request.h \ diff --git a/src/rest.cpp b/src/rest.cpp index 11f5f2703c..d3db34764d 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -644,6 +644,45 @@ static bool rest_deploymentinfo(const std::any& context, HTTPRequest* req, const } +static bool rest_mempool_transactions(const std::any& context, HTTPRequest* req, const std::string& str_uri_part) +{ + if (!CheckWarmup(req)) + return false; + + std::string param; + const RESTResponseFormat rf = ParseDataFormat(param, str_uri_part); + if (param != "contents" && param != "info") { + return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/mempool/transactions/.json"); + } + + const CTxMemPool* mempool = GetMemPool(context, req); + if (!mempool) return false; + + switch (rf) { + case RESTResponseFormat::JSON: { + std::string str_json; + std::string raw_sequence_start; + const bool verbose = param == "contents"; + + try { + raw_sequence_start = req->GetQueryParameter("sequence_start").value_or("0"); + } catch (const std::runtime_error& e) { + return RESTERR(req, HTTP_BAD_REQUEST, e.what()); + } + + const auto sequence_start{ToIntegral(raw_sequence_start)}; + str_json = MempoolTxsToJSON(*mempool, verbose, sequence_start.value()).write() + "\n"; + + req->WriteHeader("Content-Type", "application/json"); + req->WriteReply(HTTP_OK, str_json); + return true; + } + default: { + return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: json)"); + } + } +} + static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::string& str_uri_part) { if (!CheckWarmup(req)) @@ -1017,6 +1056,7 @@ static const struct { {"/rest/blockfilterheaders/", rest_filter_header}, {"/rest/chaininfo", rest_chaininfo}, {"/rest/mempool/", rest_mempool}, + {"/rest/mempool/transactions", rest_mempool_transactions}, {"/rest/headers/", rest_headers}, {"/rest/getutxos", rest_getutxos}, {"/rest/deploymentinfo/", rest_deploymentinfo}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 5fae898774..3d685f9502 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -333,6 +333,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "stop", 0, "wait" }, { "addnode", 2, "v2transport" }, { "addconnection", 2, "v2transport" }, + { "listmempooltransactions", 0, "start_sequence"}, + { "listmempooltransactions", 1, "verbose"}, }; // clang-format on diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 3776ce83d5..f96c5fb8a4 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -412,6 +414,100 @@ static RPCHelpMan maxmempool() }; } +static RPCHelpMan listmempooltransactions() +{ + return RPCHelpMan{"listmempooltransactions", + "\nReturns all transactions in the mempool. Can be filtered by mempool_sequence\n" + "\nAllows for syncing with current mempool entries via polling (not zmq).", + { + {"start_sequence", RPCArg::Type::NUM, RPCArg::Default{0}, "The mempool_sequence to start the results to. Defaults to 0 (zero, all transactions)."}, + {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "True for a json object, false for array of transaction ids"}, + }, + { + RPCResult{"for verbose = false", + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "mempool_sequence", "The current max mempool sequence value."}, + {RPCResult::Type::ARR, "txs", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "entry_sequence", "The mempool sequence value for this transaction entry."}, + {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + }}, + }}, + }}, + RPCResult{"for verbose = true", + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "mempool_sequence", "The current max mempool sequence value."}, + {RPCResult::Type::ARR, "txs", "", + { + {RPCResult::Type::OBJ, "", "", + { + Cat>( + { + {RPCResult::Type::NUM, "entry_sequence", "The mempool sequence value for this transaction entry."}, + }, + DecodeTxDoc(/*txid_field_doc=*/"The transaction id of the mempool transaction")), + }}, + }}, + }}, + }, + RPCExamples{ + HelpExampleCli("listmempooltransactions", "true") + + HelpExampleRpc("listmempooltransactions", "true") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + uint64_t start_mempool_sequence = 0; + if (!request.params[0].isNull()) { + start_mempool_sequence = request.params[0].getInt(); + } + + bool fVerbose = false; + if (!request.params[1].isNull()) + fVerbose = request.params[1].get_bool(); + + return MempoolTxsToJSON(EnsureAnyMemPool(request.context), fVerbose, start_mempool_sequence); + }, + }; +} + +UniValue MempoolTxsToJSON(const CTxMemPool& pool, bool verbose, uint64_t sequence_start) +{ + uint64_t mempool_sequence; + + LOCK(pool.cs); + mempool_sequence = pool.GetSequence(); + + UniValue o(UniValue::VOBJ); + o.pushKV("mempool_sequence", mempool_sequence); + + UniValue a(UniValue::VARR); + for (const CTxMemPoolEntry& e : pool.mapTx) { + UniValue txentry(UniValue::VOBJ); + + // We skip anything not requested. + if (e.GetSequence() < sequence_start) + continue; + + txentry.pushKV("entry_sequence", e.GetSequence()); + + if (verbose) { + // We could also calculate fees etc for this transaction, but yolo. + TxToUniv(e.GetTx(), /*block_hash=*/uint256::ZERO, /*entry=*/txentry, /*include_hex=*/false); + } else { + txentry.pushKV("txid", e.GetTx().GetHash().ToString()); + } + + a.push_back(txentry); + } + + o.pushKV("txs", a); + return o; +} + static RPCHelpMan getrawmempool() { return RPCHelpMan{"getrawmempool", @@ -1171,6 +1267,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t) {"blockchain", &savemempool}, {"blockchain", &maxmempool}, {"rawtransactions", &submitpackage}, + {"rawtransactions", &listmempooltransactions}, }; for (const auto& c : commands) { t.appendCommand(c.name, &c); diff --git a/src/rpc/mempool.h b/src/rpc/mempool.h index c775590b24..6b76c0e97d 100644 --- a/src/rpc/mempool.h +++ b/src/rpc/mempool.h @@ -30,4 +30,7 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool, const std::optional #include #include +#include #include #include #include @@ -99,7 +100,7 @@ static std::vector ScriptPubKeyDoc() { }; } -static std::vector DecodeTxDoc(const std::string& txid_field_doc) +std::vector DecodeTxDoc(const std::string& txid_field_doc) { return { {RPCResult::Type::STR_HEX, "txid", txid_field_doc}, diff --git a/src/rpc/rawtransaction.h b/src/rpc/rawtransaction.h new file mode 100644 index 0000000000..9b5f746223 --- /dev/null +++ b/src/rpc/rawtransaction.h @@ -0,0 +1,14 @@ +// Copyright (c) 2017-2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_RPC_RAWTRANSACTION_H +#define BITCOIN_RPC_RAWTRANSACTION_H + +#include + +struct RPCResult; + +std::vector DecodeTxDoc(const std::string& txid_field_doc); + +#endif // BITCOIN_RPC_RAWTRANSACTION_H diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 17a808d480..4bf15aaac6 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -159,6 +159,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "invalidateblock", "joinpsbts", "listbanned", + "listmempooltransactions", "listprunelocks", "logging", "maxmempool",