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()