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