Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(xrpl-rpc-spec VERSION 0.1.20 LANGUAGES CXX)
project(xrpl-rpc-spec VERSION 0.1.21 LANGUAGES CXX)

add_library(rpcspec INTERFACE)
add_library(rpcspec::rpcspec ALIAS rpcspec)
Expand Down
15 changes: 15 additions & 0 deletions include/rpcspec/Errors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,23 @@ struct Status
*/
std::optional<ExtraInfo> extraInfo;

/**
* @brief The code of a successful Status; a Status built from it converts to false.
*/
static constexpr XrpldError kOK = xrpl::RpcSuccess;

Status() = default;

/**
* @brief Construct a new Status object from an xrpld error code
*
* Its own overload so that the conversion is implicit: through @ref CombinedError it would
* take two user-defined conversions.
*
* @param code The error code
*/
/* implicit */ Status(XrpldError code) : code(code) {};

/**
* @brief Construct a new Status object
*
Expand Down
44 changes: 42 additions & 2 deletions include/rpcspec/handlers/transaction_entry/Spec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,48 @@
#include <rpcspec/VersionedSpec.hpp>
#include <rpcspec/handlers/transaction_entry/Types.hpp>

#include <expected>
#include <string>
#include <string_view>

namespace rpc::spec::handlers::transaction_entry {

/**
* @brief Converts `tx_hash` into a hash, or into `TxHashError::Malformed` if it is not one.
*
* Never fails: a malformed hash is a value, not a parse error, because xrpld reports it only after
* resolving the ledger. Clio rejects it earlier, with `uint256Hex`.
*/
struct TxHashConverter
{
/**
* @brief Identifier for this item in the schema dump ("uint256Hex").
*/
static constexpr std::string_view kName = "uint256Hex";

/**
* @brief The value this converter produces (`std::expected<xrpl::uint256, TxHashError>`).
*/
using ValueType = std::expected<xrpl::uint256, TxHashError>;

/**
* @brief Produce the field's hash, or `TxHashError::Malformed`.
*
* @tparam View The field-view type supplied by the backend.
* @param fieldView The field to read.
* @return The hash, or `TxHashError::Malformed`; never a Status.
*/
template <SomeFieldView View>
[[nodiscard]] Parsed<ValueType>
parse(View const& fieldView) const
{
xrpl::uint256 hash;
if (fieldView.isString() and hash.parseHex(std::string{fieldView.asString()}.c_str()))
return ValueType{hash};
return ValueType{std::unexpected{TxHashError::Malformed}};
}
};

/**
* @brief The spec that validates a request and parses it into `Input`.
*/
Expand All @@ -19,8 +59,8 @@ inline constexpr auto kInputSpec = spec<Input>(
field(
"tx_hash",
&Input::txHash,
withCustomError(required, rpc::kFieldNotFoundTransaction),
asUint256));
ifServerClio(withCustomError(required, rpc::kFieldNotFoundTransaction), uint256Hex),
TxHashConverter{}));

/**
* @brief Version-selecting spec (resolved from Input via specFor).
Expand Down
17 changes: 14 additions & 3 deletions include/rpcspec/handlers/transaction_entry/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
#include <rpcspec/Ledger.hpp>

#include <cstdint>
#include <optional>
#include <expected>

namespace rpc::spec::handlers::transaction_entry {

/**
* @brief Why a request's `tx_hash` could not be read as a hash.
*/
enum class TxHashError : std::uint8_t {
Missing, ///< The request has no `tx_hash`.
Malformed ///< `tx_hash` is not a hex-encoded uint256.
};

/**
* @brief Input for the 'transaction_entry' RPC command.
*/
Expand All @@ -21,9 +29,12 @@ struct Input
LedgerSpecifier ledger;

/**
* @brief Value of the `tx_hash` request field.
* @brief Value of the `tx_hash` request field, or why there is none.
*
* Clio's spec rejects a missing or malformed `tx_hash`, so Clio always gets a hash. xrpld
* reports both only after it has resolved the ledger, so its spec leaves them to the handler.
*/
xrpl::uint256 txHash;
std::expected<xrpl::uint256, TxHashError> txHash = std::unexpected{TxHashError::Missing};
};

} // namespace rpc::spec::handlers::transaction_entry
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ add_executable(
SpecNftHistoryTests.cpp
SpecNoRippleCheckTests.cpp
SpecSubscribeTests.cpp
SpecTransactionEntryTests.cpp
SpecValidatorTests.cpp
SpecVaultInfoTests.cpp
StatusPrinter.cpp
Expand Down
54 changes: 50 additions & 4 deletions tests/SpecClioHandlerErrorsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* @file
* @brief Clio-backend arms of the handler specs that branch on RPCSPEC_IS_CLIO.
*
* `vault_info`, `ledger_data` and `ledger` are the handler specs whose field
* errors differ per server. Compiled with RPCSPEC_IS_CLIO=1 (see rpcspec_clio_tests),
* this translation unit is the only place those branches run; the xrpld wording
* is pinned by SpecVaultInfoTests / SpecLedgerDataTests.
* `vault_info`, `ledger_data`, `ledger` and `transaction_entry` are the handler
* specs whose field errors differ per server. Compiled with RPCSPEC_IS_CLIO=1 (see
* rpcspec_clio_tests), this translation unit is the only place those branches run;
* the xrpld wording is pinned by SpecVaultInfoTests / SpecLedgerDataTests /
* SpecTransactionEntryTests.
*
* Keeping both sides asserted is deliberate — vault_info's error contract has
* already drifted between the two servers once.
Expand All @@ -18,6 +19,7 @@
#include <rpcspec/handlers/ledger/Spec.hpp>
#include <rpcspec/handlers/ledger_data/Spec.hpp>
#include <rpcspec/handlers/ledger_data/Types.hpp>
#include <rpcspec/handlers/transaction_entry/Spec.hpp>
#include <rpcspec/handlers/vault_info/Spec.hpp>

#include <Backend.hpp> // IWYU pragma: keep
Expand Down Expand Up @@ -48,6 +50,13 @@ parseLedgerData(std::string const& json)
return handlers::ledger_data::kInputSpec.parse(value);
}

auto
parseTransactionEntry(std::string const& json)
{
auto value = boost::json::parse(json);
return handlers::transaction_entry::kInputSpec.parse(value);
}

} // namespace

// --- vault_info: every field error collapses onto RpcMalformedRequest -------
Expand Down Expand Up @@ -154,3 +163,40 @@ TEST(LedgerSpecClio, diff_must_be_bool)
EXPECT_EQ(result.error(), rpc::XrpldError::RpcInvalidParams) << version;
}
}

// --- transaction_entry: tx_hash is rejected by the spec on Clio only --------

TEST(TransactionEntrySpecClio, valid_hash_parses)
{
auto const result = parseTransactionEntry(std::format(R"JSON({{"tx_hash": "{}"}})JSON", kHex1));
ASSERT_TRUE(result.has_value())
<< "error: " << result.error().error << " msg: " << result.error().message;
ASSERT_TRUE(result->txHash.has_value());

xrpl::uint256 expected;
ASSERT_TRUE(expected.parseHex(kHex1));
EXPECT_EQ(*result->txHash, expected);
}

TEST(TransactionEntrySpecClio, missing_hash_is_field_not_found_transaction)
{
auto const result = parseTransactionEntry(R"JSON({})JSON");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), rpc::ClioError::RpcFieldNotFoundTransaction);
}

TEST(TransactionEntrySpecClio, non_hex_hash_is_malformed)
{
auto const result = parseTransactionEntry(R"JSON({"tx_hash": "DEADBEEF"})JSON");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), rpc::XrpldError::RpcInvalidParams);
EXPECT_EQ(result.error().message, "tx_hashMalformed");
}

TEST(TransactionEntrySpecClio, non_string_hash_is_not_string)
{
auto const result = parseTransactionEntry(R"JSON({"tx_hash": 42})JSON");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), rpc::XrpldError::RpcInvalidParams);
EXPECT_EQ(result.error().message, "tx_hashNotString");
}
13 changes: 13 additions & 0 deletions tests/SpecCoreTypesTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ TEST(RpcSpec, status_default)
EXPECT_TRUE(status == xrpl::RpcSuccess);
}

TEST(RpcSpec, status_converts_from_xrpld_error_code)
{
auto const toStatus = [](rpc::Status status) { return status; };

EXPECT_FALSE(static_cast<bool>(toStatus(rpc::Status::kOK)));
EXPECT_EQ(toStatus(rpc::Status::kOK), rpc::Status{});

auto const status = toStatus(xrpl::RpcInvalidParams);
EXPECT_TRUE(static_cast<bool>(status));
EXPECT_TRUE(status == xrpl::RpcInvalidParams);
EXPECT_TRUE(status.message.empty());
}

TEST(RpcSpec, ledger_types_table)
{
constexpr auto& table = rpc::spec::kLedgerTypesTable;
Expand Down
80 changes: 80 additions & 0 deletions tests/SpecTransactionEntryTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @file
* GTest coverage for the `transaction_entry` typed spec.
* Compiled under RPCSPEC_IS_XRPLD.
*
* xrpld checks `tx_hash` only after it has resolved the ledger, so its spec
* never rejects `tx_hash`: a missing or malformed hash reaches the handler as
* a `TxHashError`. The Clio arm, which rejects both, is pinned by
* SpecClioHandlerErrorsTests.
*/

#include <boost/json/parse.hpp>

#include <gtest/gtest.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/handlers/transaction_entry/Spec.hpp>
#include <rpcspec/handlers/transaction_entry/Types.hpp>

#include <Backend.hpp> // IWYU pragma: keep
#include <xrpl_mock.hpp>

#include <format>
#include <string>

using namespace rpc::spec;
using namespace rpc::spec::handlers::transaction_entry;

namespace {

constexpr auto kHex1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";

auto
parse(std::string const& json)
{
auto value = boost::json::parse(json);
return kInputSpec.parse(value);
}

} // namespace

TEST(TransactionEntrySpec, valid_hash_parses)
{
auto const result = parse(std::format(R"JSON({{"tx_hash": "{}"}})JSON", kHex1));
ASSERT_TRUE(result.has_value())
<< "error: " << result.error().error << " msg: " << result.error().message;
ASSERT_TRUE(result->txHash.has_value());

xrpl::uint256 expected;
ASSERT_TRUE(expected.parseHex(kHex1));
EXPECT_EQ(*result->txHash, expected);
}

TEST(TransactionEntrySpec, missing_hash_is_left_to_the_handler)
{
auto const result = parse(R"JSON({})JSON");
ASSERT_TRUE(result.has_value())
<< "error: " << result.error().error << " msg: " << result.error().message;
ASSERT_FALSE(result->txHash.has_value());
EXPECT_EQ(result->txHash.error(), TxHashError::Missing);
}

TEST(TransactionEntrySpec, malformed_hash_is_left_to_the_handler)
{
for (auto const* hash :
{R"JSON("DEADBEEF")JSON", R"JSON("")JSON", "42", "true", "null", "[]", "{}"})
{
auto const result = parse(std::format(R"JSON({{"tx_hash": {}}})JSON", hash));
ASSERT_TRUE(result.has_value())
<< hash << " error: " << result.error().error << " msg: " << result.error().message;
ASSERT_FALSE(result->txHash.has_value()) << hash;
EXPECT_EQ(result->txHash.error(), TxHashError::Malformed) << hash;
}
}

TEST(TransactionEntrySpec, bad_ledger_selector_still_fails)
{
auto const result = parse(R"JSON({"ledger_index": "potato"})JSON");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), rpc::XrpldError::RpcInvalidParams);
}
Loading