diff --git a/Makefile.am b/Makefile.am index 4f25a48e7..bf2259303 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,6 +70,7 @@ src_libbitcoin_node_la_SOURCES = \ src/protocols/protocol_performer.cpp \ src/protocols/protocol_transaction_in_106.cpp \ src/protocols/protocol_transaction_out_106.cpp \ + src/rest/rest.cpp \ src/sessions/session.cpp \ src/sessions/session_inbound.cpp \ src/sessions/session_manual.cpp \ @@ -106,6 +107,7 @@ test_libbitcoin_node_test_SOURCES = \ test/chasers/chaser_transaction.cpp \ test/chasers/chaser_validate.cpp \ test/protocols/protocol.cpp \ + test/rest/rest.cpp \ test/sessions/session.cpp endif WITH_TESTS @@ -225,6 +227,10 @@ include_bitcoin_node_protocols_HEADERS = \ include/bitcoin/node/protocols/protocol_ws.hpp \ include/bitcoin/node/protocols/protocols.hpp +include_bitcoin_node_restdir = ${includedir}/bitcoin/node/rest +include_bitcoin_node_rest_HEADERS = \ + include/bitcoin/node/rest/rest.hpp + include_bitcoin_node_sessionsdir = ${includedir}/bitcoin/node/sessions include_bitcoin_node_sessions_HEADERS = \ include/bitcoin/node/sessions/session.hpp \ diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 6338c726f..3cd13f0b6 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -282,6 +282,7 @@ add_library( ${CANONICAL_LIB_NAME} "../../src/protocols/protocol_performer.cpp" "../../src/protocols/protocol_transaction_in_106.cpp" "../../src/protocols/protocol_transaction_out_106.cpp" + "../../src/rest/rest.cpp" "../../src/sessions/session.cpp" "../../src/sessions/session_inbound.cpp" "../../src/sessions/session_manual.cpp" @@ -348,6 +349,7 @@ if (with-tests) "../../test/chasers/chaser_transaction.cpp" "../../test/chasers/chaser_validate.cpp" "../../test/protocols/protocol.cpp" + "../../test/rest/rest.cpp" "../../test/sessions/session.cpp" ) add_test( NAME libbitcoin-node-test COMMAND libbitcoin-node-test diff --git a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj index 3e08a8b0d..7fe9a4f71 100644 --- a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj @@ -135,6 +135,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters index 81248d215..cde5b15be 100644 --- a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters @@ -16,9 +16,12 @@ {4BD50864-D3BC-4F64-0000-000000000002} - + {4BD50864-D3BC-4F64-0000-000000000003} + + {4BD50864-D3BC-4F64-0000-000000000004} + @@ -72,6 +75,9 @@ src\protocols + + src\rest + src\sessions diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj index 59ac3086b..8912f2117 100644 --- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj @@ -155,6 +155,7 @@ + @@ -217,6 +218,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters index a33714e75..4387b6442 100644 --- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters @@ -8,34 +8,37 @@ - {5FFB5F52-0772-4404-0000-000000000005} + {5FFB5F52-0772-4404-0000-000000000006} - {5FFB5F52-0772-4404-0000-000000000006} + {5FFB5F52-0772-4404-0000-000000000007} - {5FFB5F52-0772-4404-0000-000000000007} + {5FFB5F52-0772-4404-0000-000000000008} - {5FFB5F52-0772-4404-0000-000000000008} + {5FFB5F52-0772-4404-0000-000000000009} - {5FFB5F52-0772-4404-0000-000000000009} + {5FFB5F52-0772-4404-0000-00000000000A} - {5FFB5F52-0772-4404-0000-00000000000A} + {5FFB5F52-0772-4404-0000-00000000000B} - {5FFB5F52-0772-4404-0000-00000000000D} + {5FFB5F52-0772-4404-0000-00000000000F} - {5FFB5F52-0772-4404-0000-00000000000B} + {5FFB5F52-0772-4404-0000-00000000000C} + + + {5FFB5F52-0772-4404-0000-00000000000D} - {5FFB5F52-0772-4404-0000-00000000000C} + {5FFB5F52-0772-4404-0000-00000000000E} - {5FFB5F52-0772-4404-0000-00000000000E} + {5FFB5F52-0772-4404-0000-000000000001} {5FFB5F52-0772-4404-0000-000000000000} @@ -49,9 +52,12 @@ {5FFB5F52-0772-4404-0000-000000000003} - + {5FFB5F52-0772-4404-0000-000000000004} + + {5FFB5F52-0772-4404-0000-000000000005} + @@ -156,6 +162,9 @@ src\protocols + + src\rest + src\sessions @@ -338,6 +347,9 @@ include\bitcoin\node\protocols + + include\bitcoin\node\rest + include\bitcoin\node\sessions diff --git a/include/bitcoin/node.hpp b/include/bitcoin/node.hpp index 98189ce11..1108661f9 100644 --- a/include/bitcoin/node.hpp +++ b/include/bitcoin/node.hpp @@ -71,6 +71,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index 7294aed93..abea94cea 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -98,7 +98,24 @@ enum error_t : uint8_t confirm10, confirm11, confirm12, - confirm13 + confirm13, + + /// server (url parse codes) + empty_path, + invalid_number, + invalid_hash, + missing_version, + missing_target, + invalid_target, + missing_hash, + missing_height, + missing_position, + missing_id_type, + invalid_id_type, + missing_component, + invalid_component, + invalid_subcomponent, + extra_segment }; // No current need for error_code equivalence mapping. diff --git a/include/bitcoin/node/full_node.hpp b/include/bitcoin/node/full_node.hpp index fd92c6df3..3cf0bb07d 100644 --- a/include/bitcoin/node/full_node.hpp +++ b/include/bitcoin/node/full_node.hpp @@ -206,7 +206,7 @@ class BCN_API full_node chaser_template chaser_template_; chaser_snapshot chaser_snapshot_; chaser_storage chaser_storage_; - event_subscriber event_subscriber_; + event_subscriber event_subscriber_{}; }; } // namespace node diff --git a/include/bitcoin/node/rest/rest.hpp b/include/bitcoin/node/rest/rest.hpp new file mode 100644 index 000000000..1ea477858 --- /dev/null +++ b/include/bitcoin/node/rest/rest.hpp @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_NODE_REST_HPP +#define LIBBITCOIN_NODE_REST_HPP + +#include + +namespace libbitcoin { +namespace node { + +BCN_API code path_to_request(network::rpc::request_t& out, + const std::string& path) NOEXCEPT; + +} // namespace network +} // namespace libbitcoin + +#endif diff --git a/src/error.cpp b/src/error.cpp index eba9db244..d1fd7f454 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -88,7 +88,24 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { confirm10, "confirm10" }, { confirm11, "confirm11" }, { confirm12, "confirm12" }, - { confirm13, "confirm13" } + { confirm13, "confirm13" }, + + /// server (url parse codes) + { empty_path, "empty_path" }, + { invalid_number, "invalid_number" }, + { invalid_hash, "invalid_hash" }, + { missing_version, "missing_version" }, + { missing_target, "missing_target" }, + { invalid_target, "invalid_target" }, + { missing_hash, "missing_hash" }, + { missing_height, "missing_height" }, + { missing_position, "missing_position" }, + { missing_id_type, "missing_id_type" }, + { invalid_id_type, "invalid_id_type" }, + { missing_component, "missing_component" }, + { invalid_component, "invalid_component" }, + { invalid_subcomponent, "invalid_subcomponent" }, + { extra_segment, "extra_segment" } }; DEFINE_ERROR_T_CATEGORY(error, "node", "node code") diff --git a/src/full_node.cpp b/src/full_node.cpp index 62e6118b3..5bed9a1b3 100644 --- a/src/full_node.cpp +++ b/src/full_node.cpp @@ -48,8 +48,7 @@ full_node::full_node(query& query, const configuration& configuration, chaser_transaction_(*this), chaser_template_(*this), chaser_snapshot_(*this), - chaser_storage_(*this), - event_subscriber_(strand()) + chaser_storage_(*this) { } diff --git a/src/rest/rest.cpp b/src/rest/rest.cpp new file mode 100644 index 000000000..3dc38b437 --- /dev/null +++ b/src/rest/rest.cpp @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include +#include +#include +#include + +namespace libbitcoin { +namespace node { + +using namespace system; +using namespace network::rpc; + +BC_PUSH_WARNING(NO_ARRAY_INDEXING) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +template +static bool to_number(Number& out, const std::string_view& token) NOEXCEPT +{ + return !token.empty() && is_ascii_numeric(token) && token.front() != '0' && + deserialize(out, token); +} + +static hash_cptr to_hash(const std::string_view& token) NOEXCEPT +{ + hash_digest out{}; + return decode_hash(out, token) ? + emplace_shared(std::move(out)) : hash_cptr{}; +} + +code path_to_request(request_t& out, const std::string& path) NOEXCEPT +{ + if (path.empty()) + return error::empty_path; + + // Avoid conflict with node type. + using object_t = network::rpc::object_t; + + // Initialize json-rpc.v2 named params message. + out = request_t + { + .jsonrpc = version::v2, + .id = null_t{}, + .method = {}, + .params = object_t{} + }; + + auto& method = out.method; + auto& params = std::get(out.params.value()); + const auto segments = split(path, "/", false, true); + BC_ASSERT(!segments.empty()); + + size_t segment{}; + if (!segments[segment].starts_with('v')) + return error::missing_version; + + uint8_t version{}; + if (!to_number(version, segments[segment++].substr(one))) + return error::invalid_number; + + params["version"] = version; + if (segment == segments.size()) + return error::missing_target; + + // transaction, address, inputs, and outputs are identical excluding names; + // input and output are identical excluding names; block is unique. + const auto target = segments[segment++]; + if (target == "transaction") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + method = "transaction"; + params["hash"] = hash; + } + else if (target == "address") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + method = "address"; + params["hash"] = hash; + } + else if (target == "inputs") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + method = "inputs"; + params["hash"] = hash; + } + else if (target == "outputs") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + method = "outputs"; + params["hash"] = hash; + } + else if (target == "input") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + if (segment == segments.size()) + return error::missing_component; + + const auto component = segments[segment++]; + if (component == "scripts") + { + method = "input_scripts"; + } + else if (component == "witnesses") + { + method = "input_witnesses"; + } + else + { + uint32_t index{}; + if (!to_number(index, component)) + return error::invalid_number; + + params["index"] = index; + if (segment == segments.size()) + { + method = "input"; + } + else + { + auto subcomponent = segments[segment++]; + if (subcomponent == "script") + method = "input_script"; + else if (subcomponent == "witness") + method = "input_witness"; + else + return error::invalid_subcomponent; + } + } + } + else if (target == "output") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + if (segment == segments.size()) + return error::missing_component; + + const auto component = segments[segment++]; + if (component == "scripts") + { + method = "output_scripts"; + } + else if (component == "spenders") + { + method = "output_spenders"; + } + else + { + uint32_t index{}; + if (!to_number(index, component)) + return error::invalid_number; + + params["index"] = index; + if (segment == segments.size()) + { + method = "output"; + } + else + { + auto subcomponent = segments[segment++]; + if (subcomponent == "script") + method = "output_script"; + else if (subcomponent == "spender") + method = "output_spender"; + else + return error::invalid_subcomponent; + } + } + } + else if (target == "block") + { + if (segment == segments.size()) + return error::missing_id_type; + + const auto by = segments[segment++]; + if (by == "hash") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + // nullables can be implicit. + ////params["height"] = null_t{}; + params["hash"] = hash; + } + else if (by == "height") + { + if (segment == segments.size()) + return error::missing_height; + + uint32_t height{}; + if (!to_number(height, segments[segment++])) + return error::invalid_number; + + // nullables can be implicit. + ////params["hash"] = null_t{}; + params["height"] = height; + } + else + { + return error::invalid_id_type; + } + + if (segment == segments.size()) + { + method = "block"; + } + else + { + const auto component = segments[segment++]; + if (component == "transaction") + { + if (segment == segments.size()) + return error::missing_position; + + uint32_t position{}; + if (!to_number(position, segments[segment++])) + return error::invalid_number; + + params["position"] = position; + method = "block_tx"; + } + else if (component == "header") + method = "header"; + else if (component == "filter") + method = "filter"; + else if (component == "transactions") + method = "block_txs"; + else + return error::invalid_component; + } + } + else + { + return error::invalid_target; + } + + return segment == segments.size() ? error::success : error::extra_segment; +} + +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace node +} // namespace libbitcoin diff --git a/test/error.cpp b/test/error.cpp index de123847f..1fe24d463 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -177,6 +177,15 @@ BOOST_AUTO_TEST_CASE(error_t__code__protocol1__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "protocol1"); } +BOOST_AUTO_TEST_CASE(error_t__code__protocol2__true_exected_message) +{ + constexpr auto value = error::protocol2; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "protocol2"); +} + BOOST_AUTO_TEST_CASE(error_t__code__header1__true_exected_message) { constexpr auto value = error::header1; @@ -186,6 +195,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__header1__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "header1"); } +// TODO: organize2-organize15 BOOST_AUTO_TEST_CASE(error_t__code__organize1__true_exected_message) { constexpr auto value = error::organize1; @@ -195,8 +205,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__organize1__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "organize1"); } -// TODO: organize2-organize15 - +// TODO: validate2-validate6 BOOST_AUTO_TEST_CASE(error_t__code__validate1__true_exected_message) { constexpr auto value = error::validate1; @@ -206,8 +215,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__validate1__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "validate1"); } -// TODO: validate2-validate6 - +// TODO: confirm2-confirm17 BOOST_AUTO_TEST_CASE(error_t__code__confirm1__true_exected_message) { constexpr auto value = error::confirm1; @@ -217,6 +225,141 @@ BOOST_AUTO_TEST_CASE(error_t__code__confirm1__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "confirm1"); } -// TODO: confirm2-confirm17 +// server (url parse codes) + +BOOST_AUTO_TEST_CASE(error_t__code__empty_path__true_exected_message) +{ + constexpr auto value = error::empty_path; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "empty_path"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_number__true_exected_message) +{ + constexpr auto value = error::invalid_number; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_number"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_hash__true_exected_message) +{ + constexpr auto value = error::invalid_hash; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_hash"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_version__true_exected_message) +{ + constexpr auto value = error::missing_version; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_version"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_target__true_exected_message) +{ + constexpr auto value = error::missing_target; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_target"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_target__true_exected_message) +{ + constexpr auto value = error::invalid_target; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_target"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_hash__true_exected_message) +{ + constexpr auto value = error::missing_hash; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_hash"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_height__true_exected_message) +{ + constexpr auto value = error::missing_height; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_height"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_position__true_exected_message) +{ + constexpr auto value = error::missing_position; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_position"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_id_type__true_exected_message) +{ + constexpr auto value = error::missing_id_type; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_id_type"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_id_type__true_exected_message) +{ + constexpr auto value = error::invalid_id_type; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_id_type"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__missing_component__true_exected_message) +{ + constexpr auto value = error::missing_component; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "missing_component"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_component__true_exected_message) +{ + constexpr auto value = error::invalid_component; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_component"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__invalid_subcomponent__true_exected_message) +{ + constexpr auto value = error::invalid_subcomponent; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "invalid_subcomponent"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__extra_segment__true_exected_message) +{ + constexpr auto value = error::extra_segment; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "extra_segment"); +} BOOST_AUTO_TEST_SUITE_END() diff --git a/test/rest/rest.cpp b/test/rest/rest.cpp new file mode 100644 index 000000000..ca9bf1c80 --- /dev/null +++ b/test/rest/rest.cpp @@ -0,0 +1,950 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +BOOST_AUTO_TEST_SUITE(rest_parser_tests) + +using namespace system; +using namespace network::rpc; +using object_t = network::rpc::object_t; + +// General errors + +BOOST_AUTO_TEST_CASE(path_to_request__empty_path__empty_path) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, ""), node::error::empty_path); +} + +BOOST_AUTO_TEST_CASE(path_to_request__missing_version__missing_version) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/"), node::error::missing_version); + BOOST_REQUIRE_EQUAL(path_to_request(out, "/block/height/123"), node::error::missing_version); +} + +BOOST_AUTO_TEST_CASE(path_to_request__invalid_version__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/vinvalid/block/height/123"), node::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(path_to_request__version_leading_zero__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v01/block/height/123"), node::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(path_to_request__missing_target__missing_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3"), node::error::missing_target); +} + +BOOST_AUTO_TEST_CASE(path_to_request__invalid_target__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/invalid"), node::error::invalid_target); +} + +// block/hash + +BOOST_AUTO_TEST_CASE(path_to_request__block_hash_valid__expected) +{ + const std::string path = "//v42//block//hash//0000000000000000000000000000000000000000000000000000000000000042//"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_hash_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/hash"), node::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_hash_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/hash/invalidhex"), node::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_hash_invalid_component__invalid_component) +{ + const std::string path = "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::invalid_component); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_invalid_id_type__invalid_id_type) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/invalid/123"), node::error::invalid_id_type); +} + +// header/height + +BOOST_AUTO_TEST_CASE(path_to_request__header_height_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, "/v42/block/height/123456/header/")); + BOOST_REQUIRE_EQUAL(request.method, "header"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto height = std::get(object.at("height").value()); + BOOST_REQUIRE_EQUAL(height, 123456u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__header_height_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/header/extra"), node::error::extra_segment); +} + +// header/hash + +BOOST_AUTO_TEST_CASE(path_to_request__header_hash_valid__expected) +{ + const std::string path = "v42/block/hash/0000000000000000000000000000000000000000000000000000000000000042/header"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "header"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__header_hash_extra_segment__extra_segment) +{ + const std::string path = "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/header/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// filter/height + +BOOST_AUTO_TEST_CASE(path_to_request__filter_height_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, "v42/block/height/123456/filter/")); + BOOST_REQUIRE_EQUAL(request.method, "filter"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto height = std::get(object.at("height").value()); + BOOST_REQUIRE_EQUAL(height, 123456u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__filter_height_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/filter/extra"), node::error::extra_segment); +} + +// filter/hash + +BOOST_AUTO_TEST_CASE(path_to_request__filter_hash_valid__expected) +{ + const std::string path = "/v42/block/hash/0000000000000000000000000000000000000000000000000000000000000042/filter"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "filter"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__filter_hash_extra_segment__extra_segment) +{ + const std::string path = "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/filter/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// block_txs/height + +BOOST_AUTO_TEST_CASE(path_to_request__block_txs_height_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, "/v42/block/height/123456/transactions")); + BOOST_REQUIRE_EQUAL(request.method, "block_txs"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto height = std::get(object.at("height").value()); + BOOST_REQUIRE_EQUAL(height, 123456u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_txs_height_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/transactions/extra"), node::error::extra_segment); +} + +// block_txs/hash + +BOOST_AUTO_TEST_CASE(path_to_request__block_txs_hash_valid__expected) +{ + const std::string path = "/v42/block/hash/0000000000000000000000000000000000000000000000000000000000000042/transactions"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_txs"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_txs_hash_extra_segment__extra_segment) +{ + const std::string path = "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/transactions/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// block_tx/height + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_height_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, "/v42/block/height/123456/transaction/7")); + BOOST_REQUIRE_EQUAL(request.method, "block_tx"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto height = std::get(object.at("height").value()); + BOOST_REQUIRE_EQUAL(height, 123456u); + + const auto position = std::get(object.at("position").value()); + BOOST_REQUIRE_EQUAL(position, 7u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_height_missing_position__missing_position) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/transaction"), node::error::missing_position); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_height_invalid_position__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/transaction/invalid"), node::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_height_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/height/123/transaction/7/extra"), node::error::extra_segment); +} + +// block_tx/hash + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_hash_valid__expected) +{ + const std::string path = "/v42/block/hash/0000000000000000000000000000000000000000000000000000000000000042/transaction/7"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_tx"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto position = std::get(object.at("position").value()); + BOOST_REQUIRE_EQUAL(position, 7u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_hash_missing_position__missing_position) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/transaction"), node::error::missing_position); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_hash_invalid_position__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/transaction/invalid"), node::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(path_to_request__block_tx_hash_extra_segment__extra_segment) +{ + const std::string path = "/v3/block/hash/0000000000000000000000000000000000000000000000000000000000000000/transaction/7/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// inputs + +BOOST_AUTO_TEST_CASE(path_to_request__inputs_valid__expected) +{ + const std::string path = "/v255/inputs/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "inputs"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__inputs_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/inputs"), node::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__inputs_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/inputs/invalidhex"), node::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__inputs_extra_segment__extra_segment) +{ + const std::string path = "/v3/inputs/0000000000000000000000000000000000000000000000000000000000000000/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// input + +BOOST_AUTO_TEST_CASE(path_to_request__input_valid__expected) +{ + const std::string path = "/v255/input/0000000000000000000000000000000000000000000000000000000000000042/3"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/input"), node::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/input/invalidhex/3"), node::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_missing_component__missing_component) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::missing_component); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_invalid_index__invalid_number) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::invalid_number); +} + +// input_script + +BOOST_AUTO_TEST_CASE(path_to_request__input_script_valid__expected) +{ + const std::string path = "/v255/input/0000000000000000000000000000000000000000000000000000000000000042/3/script"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input_script"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_script_invalid_subcomponent__invalid_subcomponent) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/3/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_script_extra_segment__extra_segment) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/3/script/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// input_scripts + +BOOST_AUTO_TEST_CASE(path_to_request__input_scripts_valid__expected) +{ + const std::string path = "/v255/input/0000000000000000000000000000000000000000000000000000000000000042/scripts"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input_scripts"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_scripts_extra_segment__extra_segment) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/scripts/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// input_witness + +BOOST_AUTO_TEST_CASE(path_to_request__input_witness_valid__expected) +{ + const std::string path = "/v255/input/0000000000000000000000000000000000000000000000000000000000000042/3/witness"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input_witness"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_witness_extra_segment__extra_segment) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/3/witness/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// input_witnesses + +BOOST_AUTO_TEST_CASE(path_to_request__input_witnesses_valid__expected) +{ + const std::string path = "/v255/input/0000000000000000000000000000000000000000000000000000000000000042/witnesses"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input_witnesses"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__input_witnesses_extra_segment__extra_segment) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/witnesses/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// outputs + +BOOST_AUTO_TEST_CASE(path_to_request__outputs_valid__expected) +{ + const std::string path = "/v255/outputs/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "outputs"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__outputs_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/outputs"), node::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__outputs_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/outputs/invalidhex"), node::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__outputs_extra_segment__extra_segment) +{ + const std::string path = "/v3/outputs/0000000000000000000000000000000000000000000000000000000000000000/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// output + +BOOST_AUTO_TEST_CASE(path_to_request__output_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/3"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_missing_component__missing_component) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::missing_component); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_invalid_index__invalid_number) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::invalid_number); +} + +// output_script + +BOOST_AUTO_TEST_CASE(path_to_request__output_script_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/3/script"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_script"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_script_invalid_subcomponent__invalid_subcomponent) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/3/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_script_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/3/script/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// output_scripts + +BOOST_AUTO_TEST_CASE(path_to_request__output_scripts_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/scripts"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_scripts"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_scripts_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/scripts/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// output_spender + +BOOST_AUTO_TEST_CASE(path_to_request__output_spender_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/3/spender"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_spender"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_spender_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/3/spender/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// output_spenders + +BOOST_AUTO_TEST_CASE(path_to_request__output_spenders_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/spenders"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_spenders"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__output_spenders_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/spenders/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +// address + +BOOST_AUTO_TEST_CASE(path_to_request__address_valid__expected) +{ + const std::string path = "/v255/address/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!path_to_request(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "address"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); +} + +BOOST_AUTO_TEST_CASE(path_to_request__address_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/address"), node::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__address_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, "/v3/address/invalidhex"), node::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(path_to_request__address_extra_segment__extra_segment) +{ + const std::string path = "/v3/address/0000000000000000000000000000000000000000000000000000000000000000/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(path_to_request(out, path), node::error::extra_segment); +} + +BOOST_AUTO_TEST_SUITE_END()