Account tx v1 api support (#874)

* Don't fail on ledger params for v1

* Different error on invalid ledger indexes for v1

* Allow forward and binary to be not bool for v1

* Minor fixes

* Fix tests

* Don't fail if input ledger index is out of range for v1

* Restore deleted test

* Fix comparison of integers with different signedness

* Updated default api version in README and example config
This commit is contained in:
Sergey Kuznetsov
2023-09-28 11:31:35 +01:00
committed by GitHub
parent 963685dd31
commit 6ca777ea96
7 changed files with 375 additions and 144 deletions

View File

@@ -229,7 +229,7 @@ It's possible to configure `minimum`, `maximum` and `default` version like so:
"api_version": {
"min": 1,
"max": 2,
"default": 2
"default": 1
}
```
All of the above are optional.

View File

@@ -111,8 +111,8 @@
// "ssl_cert_file" : "/full/path/to/cert.file",
// "ssl_key_file" : "/full/path/to/key.file"
"api_version": {
"min": 2,
"max": 2,
"default": 2 // Clio only supports API v2 and newer
"min": 1, // Minimum API version supported (could be 1 or 2)
"max": 2, // Maximum API version supported (could be 1 or 2, but >= min)
"default": 1 // Clio behaves the same as rippled by default
}
}

46
src/rpc/common/JsonBool.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <boost/json/value_to.hpp>
namespace rpc {
/**
* @brief A wrapper around bool that allows to convert from any JSON value
*/
struct JsonBool
{
bool value = false;
operator bool() const
{
return value;
}
};
inline JsonBool
tag_invoke(boost::json::value_to_tag<JsonBool> const&, boost::json::value const& jsonValue)
{
switch (jsonValue.kind())
{
case boost::json::kind::null:
return JsonBool{false};
case boost::json::kind::bool_:
return JsonBool{jsonValue.as_bool()};
case boost::json::kind::uint64:
[[fallthrough]];
case boost::json::kind::int64:
return JsonBool{jsonValue.as_int64() != 0};
case boost::json::kind::double_:
return JsonBool{jsonValue.as_double() != 0.0};
case boost::json::kind::string:
// Also should be `jsonValue.as_string() != "false"` but rippled doesn't do that. Anyway for v2 api we have
// bool validation
return JsonBool{!jsonValue.as_string().empty() && jsonValue.as_string()[0] != 0};
case boost::json::kind::array:
return JsonBool{!jsonValue.as_array().empty()};
case boost::json::kind::object:
return JsonBool{!jsonValue.as_object().empty()};
}
throw std::runtime_error("Invalid json value");
}
} // namespace rpc

View File

@@ -76,6 +76,18 @@ struct RpcSpec final
{
}
/**
* @brief Construct a full RPC request specification from another spec and additional fields.
*
* @param other The other spec to copy fields from
* @param additionalFields The additional fields to add to the spec
*/
RpcSpec(const RpcSpec& other, std::initializer_list<FieldSpec> additionalFields) : fields_{other.fields_}
{
for (auto& f : additionalFields)
fields_.push_back(std::move(f));
}
/**
* @brief Processos the passed JSON value using the stored field specs.
*

View File

@@ -72,27 +72,39 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) con
if (input.ledgerIndexMin)
{
if (range->maxSequence < input.ledgerIndexMin || range->minSequence > input.ledgerIndexMin)
if (ctx.apiVersion > 1u &&
(input.ledgerIndexMin > range->maxSequence || input.ledgerIndexMin < range->minSequence))
{
return Error{Status{RippledError::rpcLGR_IDX_MALFORMED, "ledgerSeqMinOutOfRange"}};
}
if (static_cast<std::uint32_t>(*input.ledgerIndexMin) > minIndex)
minIndex = *input.ledgerIndexMin;
}
if (input.ledgerIndexMax)
{
if (range->maxSequence < input.ledgerIndexMax || range->minSequence > input.ledgerIndexMax)
if (ctx.apiVersion > 1u &&
(input.ledgerIndexMax > range->maxSequence || input.ledgerIndexMax < range->minSequence))
{
return Error{Status{RippledError::rpcLGR_IDX_MALFORMED, "ledgerSeqMaxOutOfRange"}};
}
if (static_cast<std::uint32_t>(*input.ledgerIndexMax) < maxIndex)
maxIndex = *input.ledgerIndexMax;
}
if (minIndex > maxIndex)
{
if (ctx.apiVersion == 1u)
return Error{Status{RippledError::rpcLGR_IDXS_INVALID}};
return Error{Status{RippledError::rpcINVALID_LGR_RANGE}};
}
if (input.ledgerHash || input.ledgerIndex || input.usingValidatedLedger)
{
// rippled does not have this check
if (input.ledgerIndexMax || input.ledgerIndexMin)
if (ctx.apiVersion > 1u && (input.ledgerIndexMax || input.ledgerIndexMin))
return Error{Status{RippledError::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"}};
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq(
@@ -247,10 +259,10 @@ tag_invoke(boost::json::value_to_tag<AccountTxHandler::Input>, boost::json::valu
}
if (jsonObject.contains(JS(binary)))
input.binary = jsonObject.at(JS(binary)).as_bool();
input.binary = boost::json::value_to<JsonBool>(jsonObject.at(JS(binary)));
if (jsonObject.contains(JS(forward)))
input.forward = jsonObject.at(JS(forward)).as_bool();
input.forward = boost::json::value_to<JsonBool>(jsonObject.at(JS(forward)));
if (jsonObject.contains(JS(limit)))
input.limit = jsonObject.at(JS(limit)).as_int64();

View File

@@ -21,6 +21,7 @@
#include <data/BackendInterface.h>
#include <rpc/RPCHelpers.h>
#include <rpc/common/JsonBool.h>
#include <rpc/common/MetaProcessors.h>
#include <rpc/common/Modifiers.h>
#include <rpc/common/Types.h>
@@ -76,8 +77,8 @@ public:
std::optional<int32_t> ledgerIndexMin;
std::optional<int32_t> ledgerIndexMax;
bool usingValidatedLedger = false;
bool binary = false;
bool forward = false;
JsonBool binary{false};
JsonBool forward{false};
std::optional<uint32_t> limit;
std::optional<Marker> marker;
std::optional<ripple::TxType> transactionType;
@@ -92,14 +93,12 @@ public:
RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion) const
{
static auto const rpcSpec = RpcSpec{
static auto const rpcSpecForV1 = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator},
{JS(ledger_hash), validation::Uint256HexStringValidator},
{JS(ledger_index), validation::LedgerIndexValidator},
{JS(ledger_index_min), validation::Type<int32_t>{}},
{JS(ledger_index_max), validation::Type<int32_t>{}},
{JS(binary), validation::Type<bool>{}},
{JS(forward), validation::Type<bool>{}},
{JS(limit),
validation::Type<uint32_t>{},
validation::Min(1u),
@@ -121,7 +120,14 @@ public:
},
};
return rpcSpec;
static auto const rpcSpec = RpcSpec{
rpcSpecForV1,
{
{JS(binary), validation::Type<bool>{}},
{JS(forward), validation::Type<bool>{}},
}};
return apiVersion == 1 ? rpcSpecForV1 : rpcSpec;
}
Result

View File

@@ -45,8 +45,9 @@ struct AccountTxParamTestCaseBundle
{
std::string testName;
std::string testJson;
std::string expectedError;
std::string expectedErrorMessage;
std::optional<std::string> expectedError;
std::optional<std::string> expectedErrorMessage;
std::uint32_t apiVersion = 2;
};
// parameterized test cases for parameters check
@@ -58,27 +59,38 @@ struct AccountTxParameterTest : public RPCAccountTxHandlerTest, public WithParam
std::string
operator()(const testing::TestParamInfo<ParamType>& info) const
{
auto bundle = static_cast<AccountTxParamTestCaseBundle>(info.param);
return bundle.testName;
return info.param.testName;
}
};
};
static auto
generateTestValuesForParametersTest()
{
return std::vector<AccountTxParamTestCaseBundle>{
AccountTxParamTestCaseBundle{"MissingAccount", R"({})", "invalidParams", "Required field 'account' missing"},
AccountTxParamTestCaseBundle{
"MissingAccount", R"({})", "invalidParams", "Required field 'account' missing"},
AccountTxParamTestCaseBundle{
"BinaryNotBool",
R"({"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "binary": 1})",
"invalidParams",
"Invalid parameters."},
AccountTxParamTestCaseBundle{
"BinaryNotBool_API_v1",
R"({"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "binary": 1})",
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"ForwardNotBool",
R"({"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "forward": 1})",
"invalidParams",
"Invalid parameters."},
AccountTxParamTestCaseBundle{
"ForwardNotBool_API_v1",
R"({"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "forward": 1})",
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"ledger_index_minNotInt",
R"({"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "ledger_index_min": "x"})",
@@ -180,6 +192,66 @@ generateTestValuesForParametersTest()
})",
"lgrIdxMalformed",
"ledgerSeqMaxOutOfRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxLargeThanMaxSeq_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 31
})",
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxSmallerThanMinSeq",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 9
})",
"lgrIdxMalformed",
"ledgerSeqMaxOutOfRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxSmallerThanMinSeq_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 9
})",
"lgrIdxsInvalid",
"Ledger indexes invalid.",
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMinSmallerThanMinSeq",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 9
})",
"lgrIdxMalformed",
"ledgerSeqMinOutOfRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMinSmallerThanMinSeq_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 9
})",
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMinLargerThanMaxSeq",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 31
})",
"lgrIdxMalformed",
"ledgerSeqMinOutOfRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMinLargerThanMaxSeq_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 31
})",
"lgrIdxsInvalid",
"Ledger indexes invalid.",
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxLessThanLedgerIndexMin",
R"({
@@ -189,6 +261,16 @@ generateTestValuesForParametersTest()
})",
"invalidLgrRange",
"Ledger range is invalid."},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxLessThanLedgerIndexMin_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 11,
"ledger_index_min": 20
})",
"lgrIdxsInvalid",
"Ledger indexes invalid.",
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxMinAndLedgerIndex",
R"({
@@ -209,31 +291,104 @@ generateTestValuesForParametersTest()
})",
"invalidParams",
"containsLedgerSpecifierAndRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxMinAndLedgerIndex_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 20,
"ledger_index_min": 11,
"ledger_index": 10
})",
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxMinAndLedgerHash",
fmt::format(
R"({{
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 20,
"ledger_index_min": 11,
"ledger_hash": "{}"
}})",
LEDGERHASH),
"invalidParams",
"containsLedgerSpecifierAndRange"},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxMinAndLedgerHash_API_v1",
fmt::format(
R"({{
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 20,
"ledger_index_min": 11,
"ledger_hash": "{}"
}})",
LEDGERHASH),
std::nullopt,
std::nullopt,
1u},
AccountTxParamTestCaseBundle{
"LedgerIndexMaxMinAndLedgerIndexValidated_API_v1",
R"({
"account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_max": 20,
"ledger_index_min": 11,
"ledger_index": "validated"
})",
std::nullopt,
std::nullopt,
1u},
};
};
};
}
INSTANTIATE_TEST_CASE_P(
RPCAccountTxGroup1,
AccountTxParameterTest,
ValuesIn(generateTestValuesForParametersTest()),
ValuesIn(AccountTxParameterTest::generateTestValuesForParametersTest()),
AccountTxParameterTest::NameGenerator{});
TEST_P(AccountTxParameterTest, InvalidParams)
TEST_P(AccountTxParameterTest, CheckParams)
{
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
auto const testBundle = GetParam();
auto* rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
std::cout << "Before parse" << std::endl;
auto const req = json::parse(testBundle.testJson);
std::cout << "After parse" << std::endl;
if (testBundle.expectedError.has_value())
{
ASSERT_TRUE(testBundle.expectedErrorMessage.has_value());
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}};
auto const req = json::parse(testBundle.testJson);
auto const output = handler.process(req, Context{yield});
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = testBundle.apiVersion});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError);
EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage);
EXPECT_EQ(err.at("error").as_string(), *testBundle.expectedError);
EXPECT_EQ(err.at("error_message").as_string(), *testBundle.expectedErrorMessage);
});
}
else
{
if (req.as_object().contains("ledger_hash"))
{
EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).WillOnce(testing::Return(ripple::LedgerHeader{}));
}
else if (req.as_object().contains("ledger_index"))
{
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(testing::Return(ripple::LedgerHeader{}));
}
EXPECT_CALL(*rawBackendPtr, fetchAccountTransactions);
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}};
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = testBundle.apiVersion});
EXPECT_TRUE(output);
});
}
}
namespace {