From 6fee4ad8cfdbdbc6a4cfab7e8977325284857f2d Mon Sep 17 00:00:00 2001 From: evoskuil Date: Thu, 14 May 2026 02:23:07 -0400 Subject: [PATCH] Add native interface subscriber parse and tests. --- src/parsers/native_target.cpp | 204 +++++++++++++++++++-------------- test/parsers/native_target.cpp | 192 ++++++++++++++++++++++++++++++- 2 files changed, 308 insertions(+), 88 deletions(-) diff --git a/src/parsers/native_target.cpp b/src/parsers/native_target.cpp index 98448854..e88f0342 100644 --- a/src/parsers/native_target.cpp +++ b/src/parsers/native_target.cpp @@ -81,8 +81,6 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT 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 == "configuration") { @@ -90,14 +88,24 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT } else if (target == "top") { - method = "top"; + if (segment == segments.size()) + { + method = "top"; + } + else + { + const auto subcomponent = segments[segment++]; + if (subcomponent == "subscribe") + method = "top_subscribe"; + else + return error::invalid_subcomponent; + } } else if (target == "address") { if (segment == segments.size()) return error::missing_hash; - // address hash is a single sha256, in reversed display endianness. const auto base16 = to_hash(segments[segment++]); if (!base16) return error::invalid_hash; @@ -115,6 +123,8 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT method = "address_unconfirmed"; else if (subcomponent == "balance") method = "address_balance"; + else if (subcomponent == "subscribe") + method = "address_subscribe"; else return error::invalid_subcomponent; } @@ -151,6 +161,8 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT method = "input_script"; else if (subcomponent == "witness") method = "input_witness"; + else if (subcomponent == "subscribe") + method = "input_subscribe"; else return error::invalid_subcomponent; } @@ -190,6 +202,8 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT method = "output_spender"; else if (subcomponent == "spenders") method = "output_spenders"; + else if (subcomponent == "subscribe") + method = "output_subscribe"; else return error::invalid_subcomponent; } @@ -200,23 +214,32 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT 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()) + const auto next = segments[segment]; + if (next == "subscribe") { - method = "tx"; + segment++; + method = "tx_subscribe"; } else { - const auto component = segments[segment++]; - if (component == "header") - method = "tx_header"; - else if (component == "details") - method = "tx_details"; + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + if (segment == segments.size()) + { + method = "tx"; + } else - return error::invalid_component; + { + const auto component = segments[segment++]; + if (component == "header") + method = "tx_header"; + else if (component == "details") + method = "tx_details"; + else + return error::invalid_component; + } } } else if (target == "block") @@ -224,98 +247,107 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT if (segment == segments.size()) return error::missing_id_type; - const auto by = segments[segment++]; - if (by == "hash") + const auto by = segments[segment]; + if (by == "subscribe") { - if (segment == segments.size()) - return error::missing_hash; - - const auto hash = to_hash(segments[segment++]); - if (!hash) return error::invalid_hash; - - 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; - - params["height"] = height; + segment++; + method = "block_subscribe"; } else { - return error::invalid_id_type; - } + segment++; // consume the identifier type + if (by == "hash") + { + if (segment == segments.size()) + return error::missing_hash; - if (segment == segments.size()) - { - method = "block"; - } - else - { - const auto component = segments[segment++]; - if (component == "tx") + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + } + else if (by == "height") { if (segment == segments.size()) - return error::missing_position; + return error::missing_height; - uint32_t position{}; - if (!to_number(position, segments[segment++])) + uint32_t height{}; + if (!to_number(height, segments[segment++])) return error::invalid_number; - params["position"] = position; - method = "block_tx"; + params["height"] = height; } - else if (component == "header") + else { - if (segment == segments.size()) - { - method = "block_header"; - } - else - { - const auto subcomponent = segments[segment++]; - if (subcomponent == "context") - method = "block_header_context"; - else - return error::invalid_subcomponent; - } + return error::invalid_id_type; } - else if (component == "txs") - method = "block_txs"; - else if (component == "details") - method = "block_details"; - else if (component == "filter") + + if (segment == segments.size()) { - if (segment == segments.size()) - return error::missing_type_id; + method = "block"; + } + else + { + const auto component = segments[segment++]; + if (component == "tx") + { + if (segment == segments.size()) + return error::missing_position; - uint8_t type{}; - if (!to_number(type, segments[segment++])) - return error::invalid_number; + uint32_t position{}; + if (!to_number(position, segments[segment++])) + return error::invalid_number; - params["type"] = type; - if (segment == segments.size()) + params["position"] = position; + method = "block_tx"; + } + else if (component == "header") { - method = "block_filter"; + if (segment == segments.size()) + { + method = "block_header"; + } + else + { + const auto subcomponent = segments[segment++]; + if (subcomponent == "context") + method = "block_header_context"; + else + return error::invalid_subcomponent; + } } - else + else if (component == "txs") + method = "block_txs"; + else if (component == "details") + method = "block_details"; + else if (component == "filter") { - const auto subcomponent = segments[segment++]; - if (subcomponent == "hash") - method = "block_filter_hash"; - else if (subcomponent == "header") - method = "block_filter_header"; + if (segment == segments.size()) + return error::missing_type_id; + + uint8_t type{}; + if (!to_number(type, segments[segment++])) + return error::invalid_number; + + params["type"] = type; + if (segment == segments.size()) + { + method = "block_filter"; + } else - return error::invalid_subcomponent; + { + const auto subcomponent = segments[segment++]; + if (subcomponent == "hash") + method = "block_filter_hash"; + else if (subcomponent == "header") + method = "block_filter_header"; + else + return error::invalid_subcomponent; + } } + else + return error::invalid_component; } - else - return error::invalid_component; } } else diff --git a/test/parsers/native_target.cpp b/test/parsers/native_target.cpp index ba5d4c8a..7e9f7a23 100644 --- a/test/parsers/native_target.cpp +++ b/test/parsers/native_target.cpp @@ -112,9 +112,30 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__top_valid__expected) BOOST_REQUIRE_EQUAL(version, 42u); } -BOOST_AUTO_TEST_CASE(parsers__native_target__top_extra_segment__extra_segment) +// top/subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__top_subscribe_valid__expected) +{ + const std::string path = "/v42/top/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "top_subscribe"); + 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(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__top_subscribe_extra_segment__extra_segment) { - const std::string path = "/v3/top/extra"; + const std::string path = "/v3/top/subscribe/extra"; request_t out{}; BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } @@ -495,6 +516,34 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__block_tx_hash_extra_segment__extra_ BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } +// block/subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__block_subscribe_valid__expected) +{ + const std::string path = "/v42/block/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_subscribe"); + 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(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__block_subscribe_extra_segment__extra_segment) +{ + const std::string path = "/v3/block/subscribe/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + // tx BOOST_AUTO_TEST_CASE(parsers__native_target__tx_valid__expected) @@ -584,6 +633,34 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__tx_header_extra_segment__extra_segm BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } +// tx/subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__tx_subscribe_valid__expected) +{ + const std::string path = "/v42/tx/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_subscribe"); + 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(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__tx_subscribe_extra_segment__extra_segment) +{ + const std::string path = "/v3/tx/subscribe/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + // inputs BOOST_AUTO_TEST_CASE(parsers__native_target__inputs_valid__expected) @@ -757,6 +834,44 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__input_witness_extra_segment__extra_ BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } +// input_subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__input_subscribe_valid__expected) +{ + const std::string path = "/v42/input/0000000000000000000000000000000000000000000000000000000000000042/3/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "input_subscribe"); + 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 index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__input_subscribe_extra_segment__extra_segment) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000042/3/subscribe/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + // outputs BOOST_AUTO_TEST_CASE(parsers__native_target__outputs_valid__expected) @@ -963,6 +1078,44 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__output_spenders_extra_segment__extr BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } +// output_subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__output_subscribe_valid__expected) +{ + const std::string path = "/v42/output/0000000000000000000000000000000000000000000000000000000000000042/3/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_subscribe"); + 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 index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__output_subscribe_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000042/3/subscribe/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + // address BOOST_AUTO_TEST_CASE(parsers__native_target__address_valid__reversed_expected) @@ -1015,6 +1168,41 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__address_invalid_subcomponent__inval // address/unconfirmed // address/balance +// address/subscribe + +BOOST_AUTO_TEST_CASE(parsers__native_target__address_subscribe_valid__expected) +{ + const std::string path = "/v42/address/0000000000000000000000000000000000000000000000000000000000000042/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "address_subscribe"); + 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(parsers__native_target__address_subscribe_extra_segment__extra_segment) +{ + const std::string path = "/v3/address/0000000000000000000000000000000000000000000000000000000000000042/subscribe/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + // block_filter/height BOOST_AUTO_TEST_CASE(parsers__native_target__block_filter_height_valid__expected)