diff --git a/doc/REST-interface.md b/doc/REST-interface.md index 6664bc2a3ae1d1..c43a80182f918f 100644 --- a/doc/REST-interface.md +++ b/doc/REST-interface.md @@ -143,6 +143,12 @@ Refer to the `getrawmempool` RPC help for details. Defaults to setting *Query parameters for `verbose` and `mempool_sequence` available in 25.0 and up.* +#### Broadcast +`POST /rest/broadcast.hex` + +Broadcasts a transaction. +The transaction hex must be passed in the body of the request. +Returns the txid if the transaction was broadcasted correctly, responds with 400 if the transaction hex can't be parsed or if broadcasting failed. Risks ------------- diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp index 0f45da45dbdbb6..39c4a9819b1341 100644 --- a/src/node/transaction.cpp +++ b/src/node/transaction.cpp @@ -33,9 +33,9 @@ static TransactionError HandleATMPError(const TxValidationState& state, std::str TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback) { - // BroadcastTransaction can be called by RPC or by the wallet. - // chainman, mempool and peerman are initialized before the RPC server and wallet are started - // and reset after the RPC sever and wallet are stopped. + // BroadcastTransaction can be called by RPC, REST, or by the wallet. + // chainman, mempool and peerman are initialized before the RPC/REST server and wallet are started + // and reset after the RPC/REST sever and wallet are stopped. assert(node.chainman); assert(node.mempool); assert(node.peerman); diff --git a/src/rest.cpp b/src/rest.cpp index 4732922a156da7..9cf850bcff86b2 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -3,10 +3,13 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "node/transaction.h" #include // IWYU pragma: keep #include +#include "logging.h" +#include "node/types.h" #include #include #include @@ -1006,6 +1009,46 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req, } } +static bool rest_broadcast(const std::any& context, HTTPRequest* req, const std::string& str_uri_part) { + if(!CheckWarmup(req)) + return false; + std::string body = req->ReadBody(); + std::string params; + const RESTResponseFormat rf = ParseDataFormat(params, str_uri_part); + if (params != "") { + return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/broadcast.hex"); + } + + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, body)) { + return RESTERR(req, HTTP_BAD_REQUEST, "TX decode failed"); + } + + const CTransactionRef tx(MakeTransactionRef(std::move(mtx))); + std::string err_string; + + NodeContext* node = GetNodeContext(context, req); + if(!node) return false; + + const node::TransactionError error = node::BroadcastTransaction(*node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true); + + if(node::TransactionError::OK != error) { + req->WriteHeader("Content-Type", "text/plain"); + req->WriteReply(HTTP_BAD_REQUEST, "Error while broadcasting: " + err_string); + } + + switch (rf) { + case RESTResponseFormat::HEX: { + req->WriteHeader("Content-Type", "text/plain"); + req->WriteReply(HTTP_OK, tx->GetHash().GetHex() + "\n"); + return true; + } + default: { + return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: hex)"); + } + } +} + static const struct { const char* prefix; bool (*handler)(const std::any& context, HTTPRequest* req, const std::string& strReq); @@ -1022,6 +1065,7 @@ static const struct { {"/rest/deploymentinfo/", rest_deploymentinfo}, {"/rest/deploymentinfo", rest_deploymentinfo}, {"/rest/blockhashbyheight/", rest_blockhash_by_height}, + {"/rest/broadcast", rest_broadcast}, }; void StartREST(const std::any& context) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index ba6e960476b981..e87cfbce93e19f 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -440,5 +440,17 @@ def run_test(self): resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") + self.log.info("Test the /broadcast URI") + tx = self.wallet.create_self_transfer() + resp_hex = self.test_rest_request("/broadcast", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], ret_type=RetType.OBJ) + assert_equal(resp_hex.read().decode('utf-8').rstrip(), tx["txid"]) + self.sync_all() + assert tx["txid"] in self.nodes[0].getrawmempool() + + # Check invalid requests + self.test_rest_request("/broadcast/123", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], status=400, ret_type=RetType.OBJ) + self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.HEX, body="0000", status=400, ret_type=RetType.OBJ) + self.test_rest_request("/broadcast", http_method="GET", req_type=ReqType.HEX, status=400, ret_type=RetType.OBJ) + if __name__ == '__main__': RESTTest(__file__).main()