diff --git a/CMakeLists.txt b/CMakeLists.txt index 072597b0..4db75fe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/NFTBuyOffers.cpp src/rpc/ngHandlers/NFTSellOffers.cpp src/rpc/ngHandlers/NFTHistory.cpp + src/rpc/ngHandlers/LedgerData.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -171,6 +172,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/NFTHistoryTest.cpp unittests/rpc/handlers/SubscribeTest.cpp unittests/rpc/handlers/UnsubscribeTest.cpp + unittests/rpc/handlers/LedgerDataTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/ngHandlers/LedgerData.cpp b/src/rpc/ngHandlers/LedgerData.cpp new file mode 100644 index 00000000..c41d9e68 --- /dev/null +++ b/src/rpc/ngHandlers/LedgerData.cpp @@ -0,0 +1,221 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include + +namespace RPCng { + +LedgerDataHandler::Result +LedgerDataHandler::process(Input input, Context const& ctx) const +{ + // marker must be int if outOfOrder is true + if (input.outOfOrder && input.marker) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "outOfOrderMarkerNotInt"}}; + if (!input.outOfOrder && input.diffMarker) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "markerNotString"}}; + + auto const range = sharedPtrBackend_->fetchLedgerRange(); + auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq( + *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence); + + if (auto const status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + + Output output; + // no marker -> first call, return header information + auto header = boost::json::object(); + if ((!input.marker) && (!input.diffMarker)) + { + if (input.binary) + { + header[JS(ledger_data)] = ripple::strHex(RPC::ledgerInfoToBlob(lgrInfo)); + } + else + { + header[JS(accepted)] = true; + header[JS(account_hash)] = ripple::strHex(lgrInfo.accountHash); + header[JS(close_flags)] = lgrInfo.closeFlags; + header[JS(close_time)] = lgrInfo.closeTime.time_since_epoch().count(); + header[JS(close_time_human)] = ripple::to_string(lgrInfo.closeTime); + header[JS(close_time_resolution)] = lgrInfo.closeTimeResolution.count(); + header[JS(hash)] = ripple::strHex(lgrInfo.hash); + header[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash); + header[JS(ledger_index)] = std::to_string(lgrInfo.seq); + header[JS(parent_close_time)] = lgrInfo.parentCloseTime.time_since_epoch().count(); + header[JS(parent_hash)] = ripple::strHex(lgrInfo.parentHash); + header[JS(seqNum)] = std::to_string(lgrInfo.seq); + header[JS(totalCoins)] = ripple::to_string(lgrInfo.drops); + header[JS(total_coins)] = ripple::to_string(lgrInfo.drops); + header[JS(transaction_hash)] = ripple::strHex(lgrInfo.txHash); + } + header[JS(closed)] = true; + output.header = std::move(header); + } + else + { + if (input.marker && !sharedPtrBackend_->fetchLedgerObject(*(input.marker), lgrInfo.seq, ctx.yield)) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "markerDoesNotExist"}}; + } + + output.ledgerHash = ripple::strHex(lgrInfo.hash); + output.ledgerIndex = lgrInfo.seq; + + auto const start = std::chrono::system_clock::now(); + std::vector results; + if (input.diffMarker) + { + // keep the same logic as previous implementation + auto diff = sharedPtrBackend_->fetchLedgerDiff(*(input.diffMarker), ctx.yield); + std::vector keys; + for (auto& [key, object] : diff) + { + if (!object.size()) + keys.push_back(std::move(key)); + } + auto objs = sharedPtrBackend_->fetchLedgerObjects(keys, lgrInfo.seq, ctx.yield); + for (size_t i = 0; i < objs.size(); ++i) + { + auto& obj = objs[i]; + if (obj.size()) + results.push_back({std::move(keys[i]), std::move(obj)}); + } + if (*(input.diffMarker) > lgrInfo.seq) + output.diffMarker = *(input.diffMarker) - 1; + } + else + { + // limit's limitation is different based on binary or json + // framework can not handler the check right now, adjust the value here + auto const limit = + std::min(input.limit, input.binary ? LedgerDataHandler::LIMITBINARY : LedgerDataHandler::LIMITJSON); + auto page = sharedPtrBackend_->fetchLedgerPage(input.marker, lgrInfo.seq, limit, input.outOfOrder, ctx.yield); + results = std::move(page.objects); + if (page.cursor) + output.marker = ripple::strHex(*(page.cursor)); + else if (input.outOfOrder) + output.diffMarker = sharedPtrBackend_->fetchLedgerRange()->maxSequence; + } + auto const end = std::chrono::system_clock::now(); + + log_.debug() << "Number of results = " << results.size() << " fetched in " + << std::chrono::duration_cast(end - start).count() << " microseconds"; + + output.states.reserve(results.size()); + for (auto const& [key, object] : results) + { + ripple::STLedgerEntry const sle{ripple::SerialIter{object.data(), object.size()}, key}; + if (input.binary) + { + boost::json::object entry; + entry[JS(data)] = ripple::serializeHex(sle); + entry[JS(index)] = ripple::to_string(sle.key()); + output.states.push_back(std::move(entry)); + } + else + { + output.states.push_back(RPC::toJson(sle)); + } + } + if (input.outOfOrder) + output.cacheFull = sharedPtrBackend_->cache().isFull(); + + auto const end2 = std::chrono::system_clock::now(); + log_.debug() << "Number of results = " << results.size() << " serialized in " + << std::chrono::duration_cast(end2 - end).count() << " microseconds"; + + return output; +} + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, LedgerDataHandler::Output const& output) +{ + auto obj = boost::json::object{ + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(validated), output.validated}, + {JS(state), output.states}, + }; + if (output.header) + obj[JS(ledger)] = *(output.header); + if (output.cacheFull) + obj["cache_full"] = *(output.cacheFull); + + if (output.diffMarker) + obj[JS(marker)] = *(output.diffMarker); + else if (output.marker) + obj[JS(marker)] = *(output.marker); + + jv = std::move(obj); +} + +LedgerDataHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + LedgerDataHandler::Input input; + auto const& jsonObject = jv.as_object(); + if (jsonObject.contains(JS(binary))) + { + input.binary = jsonObject.at(JS(binary)).as_bool(); + input.limit = input.binary ? LedgerDataHandler::LIMITBINARY : LedgerDataHandler::LIMITJSON; + } + if (jsonObject.contains(JS(limit))) + { + input.limit = jsonObject.at(JS(limit)).as_int64(); + } + if (jsonObject.contains("out_of_order")) + { + input.outOfOrder = jsonObject.at("out_of_order").as_bool(); + } + if (jsonObject.contains("marker")) + { + if (jsonObject.at("marker").is_string()) + { + input.marker = ripple::uint256{jsonObject.at("marker").as_string().c_str()}; + } + else + { + input.diffMarker = jsonObject.at("marker").as_int64(); + } + } + if (jsonObject.contains(JS(ledger_hash))) + { + input.ledgerHash = jv.at(JS(ledger_hash)).as_string().c_str(); + } + if (jsonObject.contains(JS(ledger_index))) + { + if (!jsonObject.at(JS(ledger_index)).is_string()) + { + input.ledgerIndex = jv.at(JS(ledger_index)).as_int64(); + } + else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") + { + input.ledgerIndex = std::stoi(jv.at(JS(ledger_index)).as_string().c_str()); + } + } + + return input; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/LedgerData.h b/src/rpc/ngHandlers/LedgerData.h new file mode 100644 index 00000000..11ac167e --- /dev/null +++ b/src/rpc/ngHandlers/LedgerData.h @@ -0,0 +1,95 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include + +namespace RPCng { +class LedgerDataHandler +{ + std::shared_ptr sharedPtrBackend_; + clio::Logger log_{"RPC"}; + +public: + struct Output + { + uint32_t ledgerIndex; + std::string ledgerHash; + std::optional header; + boost::json::array states; + std::optional marker; + std::optional diffMarker; + std::optional cacheFull; + bool validated = true; + }; + + // TODO: Clio does not implement "type" filter + // outOfOrder only for clio, there is no document, traverse via seq diff + // outOfOrder implementation is copied from old rpc handler + struct Input + { + std::optional ledgerHash; + std::optional ledgerIndex; + bool binary = false; + uint32_t limit = LedgerDataHandler::LIMITJSON; // max 256 for json ; 2048 for binary + std::optional marker; + std::optional diffMarker; + bool outOfOrder = false; + }; + + using Result = RPCng::HandlerReturnType; + + LedgerDataHandler(std::shared_ptr const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static const auto rpcSpec = RpcSpec{ + {JS(binary), validation::Type{}}, + {"out_of_order", validation::Type{}}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(limit), validation::Type{}}, + {JS(marker), + validation::Type{}, + validation::IfType{validation::Uint256HexStringValidator}}, + }; + return rpcSpec; + } + + Result + process(Input input, Context const& ctx) const; + +private: + static uint32_t constexpr LIMITBINARY = 2048; + static uint32_t constexpr LIMITJSON = 256; + + friend void + tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); +}; +} // namespace RPCng diff --git a/unittests/rpc/handlers/LedgerDataTest.cpp b/unittests/rpc/handlers/LedgerDataTest.cpp new file mode 100644 index 00000000..a5932beb --- /dev/null +++ b/unittests/rpc/handlers/LedgerDataTest.cpp @@ -0,0 +1,477 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include + +using namespace RPCng; +namespace json = boost::json; +using namespace testing; + +constexpr static auto RANGEMIN = 10; +constexpr static auto RANGEMAX = 30; +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto INDEX1 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; +constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"; +constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F0DD"; + +class RPCLedgerDataHandlerTest : public HandlerBaseTest +{ +}; + +struct LedgerDataParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct LedgerDataParameterTest : public RPCLedgerDataHandlerTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = static_cast(info.param); + return bundle.testName; + } + }; +}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + LedgerDataParamTestCaseBundle{ + "ledger_indexInvalid", R"({"ledger_index": "x"})", "invalidParams", "ledgerIndexMalformed"}, + LedgerDataParamTestCaseBundle{ + "ledger_hashInvalid", R"({"ledger_hash": "x"})", "invalidParams", "ledger_hashMalformed"}, + LedgerDataParamTestCaseBundle{ + "ledger_hashNotString", R"({"ledger_hash": 123})", "invalidParams", "ledger_hashNotString"}, + LedgerDataParamTestCaseBundle{"binaryNotBool", R"({"binary": 123})", "invalidParams", "Invalid parameters."}, + LedgerDataParamTestCaseBundle{"limitNotInt", R"({"limit": "xxx"})", "invalidParams", "Invalid parameters."}, + LedgerDataParamTestCaseBundle{"markerInvalid", R"({"marker": "xxx"})", "invalidParams", "markerMalformed"}, + LedgerDataParamTestCaseBundle{ + "markerOutOfOrder", + R"({ + "marker": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "out_of_order": true + })", + "invalidParams", + "outOfOrderMarkerNotInt"}, + LedgerDataParamTestCaseBundle{"markerNotString", R"({"marker": 123})", "invalidParams", "markerNotString"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCLedgerDataGroup1, + LedgerDataParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + LedgerDataParameterTest::NameGenerator{}); + +TEST_P(LedgerDataParameterTest, InvalidParams) +{ + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, Context{std::ref(yield)}); + 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); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, LedgerNotExistViaIntSequence) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "ledger_index": {} + }})", + RANGEMAX)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, LedgerNotExistViaStringSequence) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "ledger_index": "{}" + }})", + RANGEMAX)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, LedgerNotExistViaHash) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "ledger_hash": "{}" + }})", + LEDGERHASH)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, MarkerNotExist) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "marker": "{}" + }})", + INDEX1)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "markerDoesNotExist"); + }); +} + +// no marker +TEST_F(RPCLedgerDataHandlerTest, NoMarker) +{ + static auto const ledgerExpected = R"({ + "accepted":true, + "account_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "close_flags":0, + "close_time":0, + "close_time_resolution":0, + "hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":"30", + "parent_close_time":0, + "parent_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "seqNum":"30", + "totalCoins":"0", + "total_coins":"0", + "transaction_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "closed":true + })"; + + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + auto limit = 10; + std::vector bbs; + + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limit); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2})); + + while (limit--) + { + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + } + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(R"({"limit":10})"); + auto output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().contains("ledger")); + //"close_time_human" 's format depends on platform, might be sightly different + EXPECT_EQ(output->as_object().at("ledger").as_object().erase("close_time_human"), 1); + EXPECT_EQ(output->as_object().at("ledger"), json::parse(ledgerExpected)); + EXPECT_EQ(output->as_object().at("marker").as_string(), INDEX2); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 10); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, OutOfOrder) +{ + static auto const ledgerExpected = R"({ + "accepted":true, + "account_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "close_flags":0, + "close_time":0, + "close_time_resolution":0, + "hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":"30", + "parent_close_time":0, + "parent_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "seqNum":"30", + "totalCoins":"0", + "total_coins":"0", + "transaction_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "closed":true + })"; + + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + // page end + // marker return seq + std::vector bbs; + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(2); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(firstKey, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2})); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(ripple::uint256{INDEX2}, RANGEMAX, _)) + .WillByDefault(Return(std::nullopt)); + + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(R"({"limit":10, "out_of_order":true})"); + auto output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().contains("ledger")); + EXPECT_EQ(output->as_object().at("ledger").as_object().erase("close_time_human"), 1); + EXPECT_EQ(output->as_object().at("ledger"), json::parse(ledgerExpected)); + EXPECT_EQ(output->as_object().at("marker").as_uint64(), RANGEMAX); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 1); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, Marker) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillByDefault( + Return(CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123) + .getSerializer() + .peekData())); + + auto limit = 10; + std::vector bbs; + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limit); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillByDefault(Return(ripple::uint256{INDEX2})); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(ripple::uint256{INDEX2}, RANGEMAX, _)) + .WillByDefault(Return(ripple::uint256{INDEX2})); + + while (limit--) + { + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + } + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "limit":10, + "marker": "{}" + }})", + INDEX1)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_FALSE(output->as_object().contains("ledger")); + EXPECT_EQ(output->as_object().at("marker").as_string(), INDEX2); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 10); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, DiffMarker) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + auto limit = 10; + std::vector los; + std::vector bbs; + + EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff).Times(1); + + while (limit--) + { + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + los.push_back(LedgerObject{ripple::uint256{INDEX2}, Blob{}}); + } + ON_CALL(*rawBackendPtr, fetchLedgerDiff(RANGEMAX, _)).WillByDefault(Return(los)); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "limit":10, + "marker": {}, + "out_of_order": true + }})", + RANGEMAX)); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_FALSE(output->as_object().contains("ledger")); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 10); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + EXPECT_FALSE(output->as_object().at("cache_full").as_bool()); + }); +} + +TEST_F(RPCLedgerDataHandlerTest, Binary) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + auto limit = 10; + std::vector bbs; + + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limit); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2})); + + while (limit--) + { + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + } + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse( + R"({ + "limit":10, + "binary": true + })"); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().contains("ledger")); + EXPECT_TRUE(output->as_object().at("ledger").as_object().contains("ledger_data")); + EXPECT_TRUE(output->as_object().at("ledger").as_object().at("closed").as_bool()); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 10); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + }); +}