diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 2f738c97..642995fc 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -61,7 +61,7 @@ project( libbitcoin-server LANGUAGES C CXX ) -set( repository_root_dir "${CMAKE_CURRENT_SOURCE_DIR}/../../" ) +set( repository_root_dir "${CMAKE_CURRENT_SOURCE_DIR}/../.." ) if ( MSVC ) set( CMAKE_STATIC_LIBRARY_PREFIX "lib" ) diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index d45ecd25..9aa4eb7d 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -64,6 +64,7 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/parsers/bitcoind_transaction.cpp \ ${srcdir}/../../src/parsers/btcd_filter.cpp \ ${srcdir}/../../src/parsers/electrum_request.cpp \ + ${srcdir}/../../src/parsers/esplora_target.cpp \ ${srcdir}/../../src/parsers/native_query.cpp \ ${srcdir}/../../src/parsers/native_target.cpp \ ${srcdir}/../../src/protocols/protocol.cpp \ @@ -187,6 +188,7 @@ include_bitcoin_server_interfaces_HEADERS = \ ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_zmq.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/btcd.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/electrum.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/esplora.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/interfaces.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/native.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/sparrow.hpp \ @@ -207,6 +209,7 @@ include_bitcoin_server_parsers_HEADERS = \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_transaction.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/btcd_filter.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/electrum_request.hpp \ + ${srcdir}/../../include/bitcoin/server/parsers/esplora_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/native_query.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/native_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/parsers.hpp @@ -312,6 +315,7 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/parsers/admin_target.cpp \ ${srcdir}/../../test/parsers/bitcoind_query.cpp \ ${srcdir}/../../test/parsers/bitcoind_target.cpp \ + ${srcdir}/../../test/parsers/esplora_target.cpp \ ${srcdir}/../../test/parsers/native_query.cpp \ ${srcdir}/../../test/parsers/native_target.cpp \ ${srcdir}/../../test/protocols/admin/admin_diagnostics.cpp \ diff --git a/builds/gnu/configure.ac b/builds/gnu/configure.ac index 8099c0ea..3a46ca96 100644 --- a/builds/gnu/configure.ac +++ b/builds/gnu/configure.ac @@ -68,7 +68,7 @@ AC_ARG_VAR([CC], "C compiler to use, such as gcc or clang") AC_ARG_VAR([CXX], "C++ compiler to use, such as g++ or clang++") AC_ARG_VAR([PKG_CONFIG_PATH], "Additional directories for package discovery.") -repository_root_dir="@abs_top_srcdir@/../.." +repository_root_dir=`cd "${srcdir}/../.." && pwd` # Check for baseline language coverage in the compiler for the C++20 standard. #------------------------------------------------------------------------------ diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj index 466b978d..ef00c842 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -128,6 +128,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 31db0f3b..3fbf5893 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -87,6 +87,9 @@ src\parsers + + src\parsers + src\parsers diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index d5a6297b..944c8613 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -136,6 +136,7 @@ + @@ -229,6 +230,7 @@ + @@ -245,6 +247,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 249da922..f12b154c 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -138,6 +138,9 @@ src\parsers + + src\parsers + src\parsers @@ -413,6 +416,9 @@ include\bitcoin\server\interfaces + + include\bitcoin\server\interfaces + include\bitcoin\server\interfaces @@ -461,6 +467,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj index bdbb36b6..37028665 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -128,6 +128,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 31db0f3b..3fbf5893 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -87,6 +87,9 @@ src\parsers + + src\parsers + src\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 51cdf45c..c8844df3 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -136,6 +136,7 @@ + @@ -229,6 +230,7 @@ + @@ -245,6 +247,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 249da922..f12b154c 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -138,6 +138,9 @@ src\parsers + + src\parsers + src\parsers @@ -413,6 +416,9 @@ include\bitcoin\server\interfaces + + include\bitcoin\server\interfaces + include\bitcoin\server\interfaces @@ -461,6 +467,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/console/executor.cpp b/console/executor.cpp index dc18ae76..cd572fac 100644 --- a/console/executor.cpp +++ b/console/executor.cpp @@ -44,9 +44,7 @@ std::optional executor::poller_thread_{}; executor& executor::factory(parser& metadata, std::istream& input, std::ostream& output, std::ostream& error) { - // Pack creation settings, stored at store create and read at load. - metadata.configured.database.envelope = database::envelope{ - metadata.configured.bitcoin, metadata.configured.database }; + metadata.configured.initialize(); // Suppress configured batch cache sizing when batching cannot run. if (!system::batched::accelerated()) diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 803e2b5c..8e4fb0c4 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/server/interfaces/esplora.hpp b/include/bitcoin/server/interfaces/esplora.hpp new file mode 100644 index 00000000..f6eb268c --- /dev/null +++ b/include/bitcoin/server/interfaces/esplora.hpp @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * 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_SERVER_INTERFACES_ESPLORA_HPP +#define LIBBITCOIN_SERVER_INTERFACES_ESPLORA_HPP + +#include +#include + +namespace libbitcoin { +namespace server { +namespace interface { + +struct esplora_methods +{ + // github.com/Blockstream/esplora/blob/master/API.md + static constexpr std::tuple methods + { + method<"tx", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"tx_status", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"tx_merkleblock_proof", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"tx_merkle_proof", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"tx_outspend", uint8_t, system::hash_cptr, uint32_t>{ "media", "hash", "index" }, + method<"tx_outspends", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"broadcast", uint8_t, string_t>{ "media", "transaction" }, + method<"broadcast_package", uint8_t, array_t>{ "media", "transactions" }, + + method<"address", uint8_t, nullable, nullable>{ "media", "hash", "address" }, + method<"address_txs", uint8_t, nullable, nullable>{ "media", "hash", "address" }, + method<"address_txs_chain", uint8_t, nullable, nullable, nullable>{ "media", "hash", "address", "last_seen" }, + method<"address_txs_mempool", uint8_t, nullable, nullable>{ "media", "hash", "address" }, + method<"address_utxo", uint8_t, nullable, nullable>{ "media", "hash", "address" }, + + method<"block", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"block_header", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"block_status", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"block_txs", uint8_t, system::hash_cptr, optional<0_u32>>{ "media", "hash", "start" }, + method<"block_txids", uint8_t, system::hash_cptr>{ "media", "hash" }, + method<"block_txid", uint8_t, system::hash_cptr, uint32_t>{ "media", "hash", "index" }, + method<"block_height", uint8_t, uint32_t>{ "media", "height" }, + method<"blocks", uint8_t, nullable>{ "media", "height" }, + method<"tip_height", uint8_t>{ "media" }, + method<"tip_hash", uint8_t>{ "media" }, + + method<"mempool", uint8_t>{ "media" }, + method<"mempool_txids", uint8_t>{ "media" }, + method<"mempool_recent", uint8_t>{ "media" }, + + method<"fee_estimates", uint8_t>{ "media" } + }; + + template + using subscriber = network::unsubscriber; + + template + using at = method_at; + + // Derive this from above in c++26 using reflection. + + using tx = at<0>; + using tx_status = at<1>; + using tx_merkleblock_proof = at<2>; + using tx_merkle_proof = at<3>; + using tx_outspend = at<4>; + using tx_outspends = at<5>; + using broadcast = at<6>; + using broadcast_package = at<7>; + + using address = at<8>; + using address_txs = at<9>; + using address_txs_chain = at<10>; + using address_txs_mempool = at<11>; + using address_utxo = at<12>; + + using block = at<13>; + using block_header = at<14>; + using block_status = at<15>; + using block_txs = at<16>; + using block_txids = at<17>; + using block_txid = at<18>; + using block_height = at<19>; + using blocks = at<20>; + using tip_height = at<21>; + using tip_hash = at<22>; + + using mempool = at<23>; + using mempool_txids = at<24>; + using mempool_recent = at<25>; + + using fee_estimates = at<26>; +}; + +/// media is implied by the target (there is no format query string). +/// --------------------------------------------------------------------------- + +/// /tx/[txhash] {1} +/// /tx/[txhash]/hex {1, text} +/// /tx/[txhash]/raw {1, data} +/// /tx/[txhash]/status {1} +/// /tx/[txhash]/merkleblock-proof {1, text} +/// /tx/[txhash]/merkle-proof {1} +/// /tx/[txhash]/outspend/[index] {1} +/// /tx/[txhash]/outspends {all outputs in the tx} +/// /tx {txhash, text} [post, body is transaction hex] +/// /txs/package {all} [post, body is array of transaction hex] + +/// --------------------------------------------------------------------------- + +/// /address/[address] {1} +/// /scripthash/[output-script-hash] {1} +/// /address/[address]/txs {25 confirmed and 50 unconfirmed} +/// /address/[address]/txs/chain {25 confirmed} +/// /address/[address]/txs/chain/[last-seen-txhash] {25 confirmed} +/// /address/[address]/txs/mempool {50 unconfirmed} +/// /address/[address]/utxo {all} + +/// --------------------------------------------------------------------------- + +/// /block/[bkhash] {1} +/// /block/[bkhash]/raw {1, data} +/// /block/[bkhash]/header {1, text} +/// /block/[bkhash]/status {1} +/// /block/[bkhash]/txs {25 txs in the block} +/// /block/[bkhash]/txs/[start-index] {25 txs in the block} +/// /block/[bkhash]/txids {all txs in the block} +/// /block/[bkhash]/txid/[index] {1, text} +/// /block-height/[height] {bkhash, text} +/// /blocks {10 from the top} +/// /blocks/[height] {10 from the height} +/// /blocks/tip/height {1, text} +/// /blocks/tip/hash {1, text} + +/// --------------------------------------------------------------------------- + +/// /mempool {1} +/// /mempool/txids {all} +/// /mempool/recent {10} + +/// /fee-estimates {1} + +} // namespace interface +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/interfaces/interfaces.hpp b/include/bitcoin/server/interfaces/interfaces.hpp index 047eca53..15f93474 100644 --- a/include/bitcoin/server/interfaces/interfaces.hpp +++ b/include/bitcoin/server/interfaces/interfaces.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ using bitcoind_zmq = publish; using btcd = publish; using electrum = publish; using electrum_handshake = publish; +using esplora = publish; using native = publish; using sparrow = publish; using stratum_v1 = publish; diff --git a/include/bitcoin/server/parsers/esplora_target.hpp b/include/bitcoin/server/parsers/esplora_target.hpp new file mode 100644 index 00000000..e105a01b --- /dev/null +++ b/include/bitcoin/server/parsers/esplora_target.hpp @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * 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_SERVER_PARSERS_ESPLORA_TARGET_HPP +#define LIBBITCOIN_SERVER_PARSERS_ESPLORA_TARGET_HPP + +#include + +namespace libbitcoin { +namespace server { + +BCS_API code esplora_target(network::rpc::request_t& out, + const std::string_view& path) NOEXCEPT; + +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/parsers/parsers.hpp b/include/bitcoin/server/parsers/parsers.hpp index 70e1a733..fb4ae74f 100644 --- a/include/bitcoin/server/parsers/parsers.hpp +++ b/include/bitcoin/server/parsers/parsers.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index 76ff8dfb..39f5a1ae 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -144,6 +144,16 @@ class BCS_API settings using base::base; }; + struct esplora_server + : public network::settings::websocket_server + { + using base = network::settings::websocket_server; + using base::base; + + /// Maximum number of address history entries upon one index walk. + uint32_t maximum_history{ 1'000'000 }; + }; + /// html (http/s) document server settings (has directory/default). /// This is for web servers that expose a local file system directory. struct html_server @@ -252,6 +262,9 @@ class BCS_API settings /// sparrow interface (electrum plus block stats and silent payments) sparrow_server sparrow{ "sparrow" }; + /// esplora compat interface (http/s + websocket, RESTful json) + esplora_server esplora{ "esplora" }; + /// stratum v1 compat interface (tcp/s, json-rpc-v1, auth handshake) network::settings::tls_server stratum_v1{ "stratum_v1" }; diff --git a/src/parser.cpp b/src/parser.cpp index 99f1d18c..e11ab2e1 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1550,6 +1550,92 @@ options_metadata parser::load_settings() THROWS value(&configured.server.sparrow.more_safes), "Advertised secure host:port at which another server can be reached (defaults to empty)." ) + /* [esplora] */ + ( + "esplora.bind", + value(&configured.server.esplora.binds), + "IP address to bind, multiple allowed, defaults to empty (disabled)." + ) + ( + "esplora.safe", + value(&configured.server.esplora.safes), + "IP address to secure bind, multiple allowed, defaults to empty (disabled)." + ) + ( + "esplora.cert_auth", + value(&configured.server.esplora.cert_auth), + "The certificate authority directory (*.PEM), enables client authentication." + ) + ( + "esplora.cert_path", + value(&configured.server.esplora.cert_path), + "The path to the server certificate file (.PEM), defaults to unused." + ) + ( + "esplora.key_path", + value(&configured.server.esplora.key_path), + "The path to the server private key file (.PEM), defaults to unused." + ) + ( + "esplora.key_pass", + value(&configured.server.esplora.key_pass), + "The password to decrypt the server private key file (.PEM), optional." + ) + ( + "esplora.connections", + value(&configured.server.esplora.connections), + "The required maximum number of connections, defaults to '0'." + ) + ( + "esplora.inactivity_minutes", + value(&configured.server.esplora.inactivity_minutes), + "The idle timeout (http keep-server), defaults to '60'." + ) + ( + "esplora.expiration_minutes", + value(&configured.server.esplora.expiration_minutes), + "The idle timeout (http keep-alive), defaults to '60'." + ) + ( + "esplora.minimum_buffer", + value(&configured.server.esplora.minimum_buffer), + "The minimum retained read buffer size, defaults to '4000000'." + ) + ( + "esplora.maximum_request", + value(&configured.server.esplora.maximum_request), + "The maximum allowed request size, defaults to '4000000'." + ) + ( + "esplora.rate_limit", + value(&configured.server.esplora.rate_limit), + "The send rate limit in bytes per second, defaults to '0' (unlimited)." + ) + ( + "esplora.server", + value(&configured.server.esplora.server), + "The server name (http header), defaults to '" BC_HTTP_SERVER_NAME "'." + ) + ( + "esplora.maximum_history", + value(&configured.server.esplora.maximum_history), + "The maximum number of address history entries, defaults to '1000000'." + ) + ( + "esplora.host", + value(&configured.server.esplora.hosts), + "The host name (http verification), multiple allowed, defaults to empty (disabled)." + ) + ( + "esplora.origin", + value(&configured.server.esplora.origins), + "The allowed origin (see CORS), multiple allowed, defaults to empty (disabled)." + ) + ( + "esplora.allow_opaque_origin", + value(&configured.server.esplora.allow_opaque_origin), + "Allow requests from opaque origin (see CORS), multiple allowed, defaults to false." + ) /* [stratum_v1] */ ( "stratum_v1.bind", diff --git a/src/parsers/esplora_target.cpp b/src/parsers/esplora_target.cpp new file mode 100644 index 00000000..7b6fb679 --- /dev/null +++ b/src/parsers/esplora_target.cpp @@ -0,0 +1,403 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * 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 + +namespace libbitcoin { +namespace server { + +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) && (is_one(token.size()) || + 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{}; +} + +static void set_media(network::rpc::object_t& params, + network::http::media_type media) NOEXCEPT +{ + params["media"] = to_value(media); +} + +code esplora_target(request_t& out, const std::string_view& path) NOEXCEPT +{ + constexpr auto text = network::http::media_type::text_plain; + constexpr auto json = network::http::media_type::application_json; + constexpr auto data = network::http::media_type::application_octet_stream; + + const auto clean = split(path, "?", false, false).front(); + if (clean.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(clean, "/", false, true); + BC_ASSERT(!segments.empty()); + + size_t segment{}; + const auto target = segments[segment++]; + if (target.empty()) + return error::missing_target; + + if (target == "tx") + { + if (segment == segments.size()) + { + set_media(params, text); + method = "broadcast"; + } + else + { + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + if (segment == segments.size()) + { + set_media(params, json); + method = "tx"; + } + else + { + const auto component = segments[segment++]; + if (component == "hex") + { + set_media(params, text); + method = "tx"; + } + else if (component == "raw") + { + set_media(params, data); + method = "tx"; + } + else if (component == "status") + { + set_media(params, json); + method = "tx_status"; + } + else if (component == "merkleblock-proof") + { + set_media(params, text); + method = "tx_merkleblock_proof"; + } + else if (component == "merkle-proof") + { + set_media(params, json); + method = "tx_merkle_proof"; + } + else if (component == "outspend") + { + if (segment == segments.size()) + return error::missing_position; + + uint32_t index{}; + if (!to_number(index, segments[segment++])) + return error::invalid_number; + + params["index"] = index; + set_media(params, json); + method = "tx_outspend"; + } + else if (component == "outspends") + { + set_media(params, json); + method = "tx_outspends"; + } + else + { + return error::invalid_component; + } + } + } + } + else if (target == "txs") + { + if (segment == segments.size()) + return error::missing_component; + + if (segments[segment++] != "package") + return error::invalid_component; + + set_media(params, json); + method = "broadcast_package"; + } + else if (target == "address" || target == "scripthash") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto key = segments[segment++]; + if (target == "scripthash") + { + const auto hash = to_hash(key); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + } + else + { + params["address"] = key; + } + + if (segment == segments.size()) + { + set_media(params, json); + method = "address"; + } + else + { + const auto component = segments[segment++]; + if (component == "txs") + { + if (segment == segments.size()) + { + set_media(params, json); + method = "address_txs"; + } + else + { + const auto subcomponent = segments[segment++]; + if (subcomponent == "chain") + { + if (segment != segments.size()) + { + const auto last = to_hash(segments[segment++]); + if (!last) return error::invalid_hash; + + params["last_seen"] = last; + } + + set_media(params, json); + method = "address_txs_chain"; + } + else if (subcomponent == "mempool") + { + set_media(params, json); + method = "address_txs_mempool"; + } + else + { + return error::invalid_subcomponent; + } + } + } + else if (component == "utxo") + { + set_media(params, json); + method = "address_utxo"; + } + else + { + return error::invalid_component; + } + } + } + else if (target == "block") + { + 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()) + { + set_media(params, json); + method = "block"; + } + else + { + const auto component = segments[segment++]; + if (component == "raw") + { + set_media(params, data); + method = "block"; + } + else if (component == "header") + { + set_media(params, text); + method = "block_header"; + } + else if (component == "status") + { + set_media(params, json); + method = "block_status"; + } + else if (component == "txs") + { + if (segment != segments.size()) + { + uint32_t start{}; + if (!to_number(start, segments[segment++])) + return error::invalid_number; + + params["start"] = start; + } + + set_media(params, json); + method = "block_txs"; + } + else if (component == "txids") + { + set_media(params, json); + method = "block_txids"; + } + else if (component == "txid") + { + if (segment == segments.size()) + return error::missing_position; + + uint32_t index{}; + if (!to_number(index, segments[segment++])) + return error::invalid_number; + + params["index"] = index; + set_media(params, text); + method = "block_txid"; + } + else + { + return error::invalid_component; + } + } + } + else if (target == "block-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; + set_media(params, text); + method = "block_height"; + } + else if (target == "blocks") + { + if (segment == segments.size()) + { + set_media(params, json); + method = "blocks"; + } + else if (segments[segment] == "tip") + { + segment++; + if (segment == segments.size()) + return error::missing_component; + + const auto subcomponent = segments[segment++]; + if (subcomponent == "height") + { + set_media(params, text); + method = "tip_height"; + } + else if (subcomponent == "hash") + { + set_media(params, text); + method = "tip_hash"; + } + else + { + return error::invalid_subcomponent; + } + } + else + { + uint32_t height{}; + if (!to_number(height, segments[segment++])) + return error::invalid_number; + + params["height"] = height; + set_media(params, json); + method = "blocks"; + } + } + else if (target == "mempool") + { + if (segment == segments.size()) + { + set_media(params, json); + method = "mempool"; + } + else + { + const auto component = segments[segment++]; + if (component == "txids") + { + set_media(params, json); + method = "mempool_txids"; + } + else if (component == "recent") + { + set_media(params, json); + method = "mempool_recent"; + } + else + { + return error::invalid_component; + } + } + } + else if (target == "fee-estimates") + { + set_media(params, json); + method = "fee_estimates"; + } + else + { + return error::invalid_target; + } + + return segment == segments.size() ? error::success : error::extra_segment; +} + +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/test/parsers/esplora_target.cpp b/test/parsers/esplora_target.cpp new file mode 100644 index 00000000..b410bf4f --- /dev/null +++ b/test/parsers/esplora_target.cpp @@ -0,0 +1,670 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * 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(esplora_target_tests) + +using namespace system; +using namespace network::rpc; +using object_t = network::rpc::object_t; + +constexpr auto text = to_value(network::http::media_type::text_plain); +constexpr auto json = to_value(network::http::media_type::application_json); +constexpr auto data = to_value(network::http::media_type::application_octet_stream); + +// General errors + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__empty_path__empty_path) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "?foo=bar"), server::error::empty_path); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__missing_target__missing_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/"), server::error::missing_target); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__invalid_target__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/invalid"), server::error::invalid_target); +} + +// tx + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_valid__expected) +{ + const std::string path = "//tx//0000000000000000000000000000000000000000000000000000000000000042//?foo=bar"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "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(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); + + 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__esplora_target__tx_hex__text) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/hex"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_raw__data) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/raw"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), data); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_status__expected) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/status"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_status"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_merkleblock_proof__text) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/merkleblock-proof"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_merkleblock_proof"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_merkle_proof__expected) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/merkle-proof"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_merkle_proof"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_outspend__expected) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/outspend/3"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_outspend"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); + BOOST_REQUIRE_EQUAL(std::get(object.at("index").value()), 3u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_outspend_missing_index__missing_position) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/tx/0000000000000000000000000000000000000000000000000000000000000042/outspend"), server::error::missing_position); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_outspend_invalid_index__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/tx/0000000000000000000000000000000000000000000000000000000000000042/outspend/x"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_outspends__expected) +{ + const std::string path = "/tx/0000000000000000000000000000000000000000000000000000000000000042/outspends"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "tx_outspends"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/tx/invalid"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_invalid_component__invalid_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/tx/0000000000000000000000000000000000000000000000000000000000000042/bogus"), server::error::invalid_component); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tx_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/tx/0000000000000000000000000000000000000000000000000000000000000042/hex/extra"), server::error::extra_segment); +} + +// broadcast + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__broadcast__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/tx")); + BOOST_REQUIRE_EQUAL(request.method, "broadcast"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__broadcast_package__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/txs/package")); + BOOST_REQUIRE_EQUAL(request.method, "broadcast_package"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__txs_missing_component__missing_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/txs"), server::error::missing_component); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__txs_invalid_component__invalid_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/txs/bogus"), server::error::invalid_component); +} + +// address + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/address/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")); + BOOST_REQUIRE_EQUAL(request.method, "address"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); + BOOST_REQUIRE_EQUAL(std::get(object.at("address").value()), "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_missing__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/address"), server::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_txs__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/address/1abc/txs")); + BOOST_REQUIRE_EQUAL(request.method, "address_txs"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_txs_chain__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/address/1abc/txs/chain")); + BOOST_REQUIRE_EQUAL(request.method, "address_txs_chain"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_txs_chain_last_seen__expected) +{ + const std::string path = "/address/1abc/txs/chain/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "address_txs_chain"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto& any = std::get(object.at("last_seen").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__esplora_target__address_txs_chain_invalid_last_seen__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/address/1abc/txs/chain/invalid"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_txs_mempool__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/address/1abc/txs/mempool")); + BOOST_REQUIRE_EQUAL(request.method, "address_txs_mempool"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_txs_invalid_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/address/1abc/txs/bogus"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_utxo__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/address/1abc/utxo")); + BOOST_REQUIRE_EQUAL(request.method, "address_utxo"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__address_invalid_component__invalid_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/address/1abc/bogus"), server::error::invalid_component); +} + +// scripthash + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__scripthash_valid__expected) +{ + const std::string path = "/scripthash/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "address"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); + + 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__esplora_target__scripthash_utxo__expected) +{ + const std::string path = "/scripthash/0000000000000000000000000000000000000000000000000000000000000042/utxo"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "address_utxo"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__scripthash_missing__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/scripthash"), server::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__scripthash_invalid__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/scripthash/invalid"), server::error::invalid_hash); +} + +// block + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_valid__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); + + 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__esplora_target__block_raw__data) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/raw"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), data); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_header__text) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/header"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_header"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_status__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/status"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_status"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txs__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/txs"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_txs"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txs_start__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/txs/25"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_txs"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + BOOST_REQUIRE_EQUAL(std::get(object.at("start").value()), 25u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txs_invalid_start__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/0000000000000000000000000000000000000000000000000000000000000042/txs/x"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txids__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/txids"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_txids"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txid__expected) +{ + const std::string path = "/block/0000000000000000000000000000000000000000000000000000000000000042/txid/7"; + + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "block_txid"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); + BOOST_REQUIRE_EQUAL(std::get(object.at("index").value()), 7u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txid_missing_index__missing_position) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/0000000000000000000000000000000000000000000000000000000000000042/txid"), server::error::missing_position); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block"), server::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/invalid"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_invalid_component__invalid_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/0000000000000000000000000000000000000000000000000000000000000042/bogus"), server::error::invalid_component); +} + +// block-height + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_height_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/block-height/123")); + BOOST_REQUIRE_EQUAL(request.method, "block_height"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); + BOOST_REQUIRE_EQUAL(std::get(object.at("height").value()), 123u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_height_zero__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/block-height/0")); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("height").value()), 0u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_height_leading_zero__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block-height/0123"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_height_missing__missing_height) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block-height"), server::error::missing_height); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_height_invalid__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block-height/x"), server::error::invalid_number); +} + +// blocks + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__blocks_valid__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/blocks")); + BOOST_REQUIRE_EQUAL(request.method, "blocks"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__blocks_height__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/blocks/100")); + BOOST_REQUIRE_EQUAL(request.method, "blocks"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(std::get(object.at("height").value()), 100u); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__blocks_invalid_height__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/blocks/x"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tip_height__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/blocks/tip/height")); + BOOST_REQUIRE_EQUAL(request.method, "tip_height"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tip_hash__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/blocks/tip/hash")); + BOOST_REQUIRE_EQUAL(request.method, "tip_hash"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), text); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tip_missing__missing_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/blocks/tip"), server::error::missing_component); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__tip_invalid__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/blocks/tip/bogus"), server::error::invalid_subcomponent); +} + +// mempool + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__mempool__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/mempool")); + BOOST_REQUIRE_EQUAL(request.method, "mempool"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__mempool_txids__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/mempool/txids")); + BOOST_REQUIRE_EQUAL(request.method, "mempool_txids"); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__mempool_recent__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/mempool/recent")); + BOOST_REQUIRE_EQUAL(request.method, "mempool_recent"); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__mempool_invalid_component__invalid_component) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/mempool/bogus"), server::error::invalid_component); +} + +// fee-estimates + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__fee_estimates__expected) +{ + request_t request{}; + BOOST_REQUIRE(!esplora_target(request, "/fee-estimates")); + BOOST_REQUIRE_EQUAL(request.method, "fee_estimates"); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + BOOST_REQUIRE_EQUAL(std::get(object.at("media").value()), json); +} + +BOOST_AUTO_TEST_CASE(parsers__esplora_target__fee_estimates_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/fee-estimates/extra"), server::error::extra_segment); +} + +BOOST_AUTO_TEST_SUITE_END()