From 0f29c86eb2e1af476acb8507939e9a18af065a67 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Tue, 8 Oct 2024 15:28:29 +0200 Subject: [PATCH] rest: Add "/broadcast" endpoint --- doc/REST-interface.md | 6 +++++ src/node/transaction.cpp | 6 ++--- src/rest.cpp | 40 +++++++++++++++++++++++++++++++ test/functional/interface_rest.py | 15 ++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/doc/REST-interface.md b/doc/REST-interface.md index 6664bc2a3ae1d1..945a4441db5c30 100644 --- a/doc/REST-interface.md +++ b/doc/REST-interface.md @@ -36,6 +36,12 @@ Responds with 404 if the transaction doesn't exist. By default, this endpoint will only search the mempool. To query for a confirmed transaction, enable the transaction index via "txindex=1" command line / configuration option. +`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. + #### Blocks - `GET /rest/block/.` - `GET /rest/block/notxdetails/.` diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp index 0f45da45dbdbb6..6e9ad91a6802a2 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 servers and wallet are started + // and reset after the RPC/REST servers 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..8d5d8e13cdab9b 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1006,6 +1007,44 @@ 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; + const 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"); + } + + switch (rf) { + case RESTResponseFormat::HEX: { + 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) { + return RESTERR(req, HTTP_BAD_REQUEST, "Error while broadcasting: " + err_string); + } + + 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 +1061,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..cbf684a2060178 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -440,5 +440,20 @@ 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 + resp = self.test_rest_request("/broadcast/123", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], status=400, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid URI format. Expected /rest/broadcast.hex") + resp = self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.HEX, body="0000", status=400, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"TX decode failed") + resp = self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.JSON, body=tx["hex"], status=404, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"output format not found (available: hex)") + if __name__ == '__main__': RESTTest(__file__).main()