From 7776a5ffb6b5d6a662e89f4e8ad3a522fad4c459 Mon Sep 17 00:00:00 2001 From: cyan317 <120398799+cindyyan317@users.noreply.github.com> Date: Tue, 2 May 2023 13:24:23 +0100 Subject: [PATCH] Init (#614) Fixes #617 --- CMakeLists.txt | 2 + src/rpc/BookChangesHelper.h | 232 +++++++++++++++++++++ src/rpc/ngHandlers/BookChanges.cpp | 76 +++++++ src/rpc/ngHandlers/BookChanges.h | 77 +++++++ src/subscriptions/SubscriptionManager.cpp | 1 + unittests/rpc/handlers/BookChangesTest.cpp | 213 +++++++++++++++++++ 6 files changed, 601 insertions(+) create mode 100644 src/rpc/BookChangesHelper.h create mode 100644 src/rpc/ngHandlers/BookChanges.cpp create mode 100644 src/rpc/ngHandlers/BookChanges.h create mode 100644 unittests/rpc/handlers/BookChangesTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 47336690..c814585a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/LedgerData.cpp src/rpc/ngHandlers/AccountNFTs.cpp src/rpc/ngHandlers/AccountObjects.cpp + src/rpc/ngHandlers/BookChanges.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -177,6 +178,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/UnsubscribeTest.cpp unittests/rpc/handlers/LedgerDataTest.cpp unittests/rpc/handlers/AccountObjectsTest.cpp + unittests/rpc/handlers/BookChangesTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/BookChangesHelper.h b/src/rpc/BookChangesHelper.h new file mode 100644 index 00000000..7b62d6fb --- /dev/null +++ b/src/rpc/BookChangesHelper.h @@ -0,0 +1,232 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { + +/** + * @brief Represents an entry in the book_changes' changes array. + */ +struct BookChange +{ + ripple::STAmount sideAVolume; + ripple::STAmount sideBVolume; + ripple::STAmount highRate; + ripple::STAmount lowRate; + ripple::STAmount openRate; + ripple::STAmount closeRate; +}; + +/** + * @brief Encapsulates the book_changes computations and transformations. + */ +class BookChanges final +{ +public: + BookChanges() = delete; // only accessed via static handle function + + /** + * @brief Computes all book_changes for the given transactions. + * + * @param transactions The transactions to compute book changes for + * @return std::vector Book changes + */ + [[nodiscard]] static std::vector + compute(std::vector const& transactions) + { + return HandlerImpl{}(transactions); + } + +private: + class HandlerImpl final + { + std::map tally_ = {}; + std::optional offerCancel_ = {}; + + public: + [[nodiscard]] std::vector + operator()(std::vector const& transactions) + { + for (auto const& tx : transactions) + handleBookChange(tx); + + // TODO: rewrite this with std::ranges when compilers catch up + std::vector changes; + std::transform( + std::make_move_iterator(std::begin(tally_)), + std::make_move_iterator(std::end(tally_)), + std::back_inserter(changes), + [](auto obj) { return obj.second; }); + return changes; + } + + private: + void + handleAffectedNode(ripple::STObject const& node) + { + auto const& metaType = node.getFName(); + auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType); + + // we only care about ripple::ltOFFER objects being modified or + // deleted + if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode) + return; + + // if either FF or PF are missing we can't compute + // but generally these are cancelled rather than crossed + // so skipping them is consistent + if (!node.isFieldPresent(ripple::sfFinalFields) || !node.isFieldPresent(ripple::sfPreviousFields)) + return; + + auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast(); + auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast(); + + // defensive case that should never be hit + if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) || + !previousFields.isFieldPresent(ripple::sfTakerGets) || + !previousFields.isFieldPresent(ripple::sfTakerPays)) + return; + + // filter out any offers deleted by explicit offer cancels + if (metaType == ripple::sfDeletedNode && offerCancel_ && + finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_) + return; + + // compute the difference in gets and pays actually + // affected onto the offer + auto const deltaGets = + finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets); + auto const deltaPays = + finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays); + + transformAndStore(deltaGets, deltaPays); + } + + void + transformAndStore(ripple::STAmount const& deltaGets, ripple::STAmount const& deltaPays) + { + auto const g = to_string(deltaGets.issue()); + auto const p = to_string(deltaPays.issue()); + + auto const noswap = isXRP(deltaGets) ? true : (isXRP(deltaPays) ? false : (g < p)); + + auto first = noswap ? deltaGets : deltaPays; + auto second = noswap ? deltaPays : deltaGets; + + // defensively programmed, should (probably) never happen + if (second == beast::zero) + return; + + auto const rate = divide(first, second, ripple::noIssue()); + + if (first < beast::zero) + first = -first; + + if (second < beast::zero) + second = -second; + + auto const key = noswap ? (g + '|' + p) : (p + '|' + g); + if (tally_.contains(key)) + { + auto& entry = tally_.at(key); + + entry.sideAVolume += first; + entry.sideBVolume += second; + + if (entry.highRate < rate) + entry.highRate = rate; + + if (entry.lowRate > rate) + entry.lowRate = rate; + + entry.closeRate = rate; + } + else + { + // TODO: use paranthesized initialization when clang catches up + tally_[key] = { + first, // sideAVolume + second, // sideBVolume + rate, // highRate + rate, // lowRate + rate, // openRate + rate, // closeRate + }; + } + } + + void + handleBookChange(Backend::TransactionAndMetadata const& blob) + { + auto const [tx, meta] = RPC::deserializeTxPlusMeta(blob); + if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType)) + return; + + offerCancel_ = shouldCancelOffer(tx); + for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes)) + handleAffectedNode(node); + } + + std::optional + shouldCancelOffer(std::shared_ptr const& tx) const + { + switch (tx->getFieldU16(ripple::sfTransactionType)) + { + // in future if any other ways emerge to cancel an offer + // this switch makes them easy to add + case ripple::ttOFFER_CANCEL: + case ripple::ttOFFER_CREATE: + if (tx->isFieldPresent(ripple::sfOfferSequence)) + return tx->getFieldU32(ripple::sfOfferSequence); + default: + return std::nullopt; + } + } + }; +}; + +inline void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change) +{ + auto amountStr = [](ripple::STAmount const& amount) -> std::string { + return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou()); + }; + + auto currencyStr = [](ripple::STAmount const& amount) -> std::string { + return isXRP(amount) ? "XRP_drops" : to_string(amount.issue()); + }; + + jv = { + {JS(currency_a), currencyStr(change.sideAVolume)}, + {JS(currency_b), currencyStr(change.sideBVolume)}, + {JS(volume_a), amountStr(change.sideAVolume)}, + {JS(volume_b), amountStr(change.sideBVolume)}, + {JS(high), to_string(change.highRate.iou())}, + {JS(low), to_string(change.lowRate.iou())}, + {JS(open), to_string(change.openRate.iou())}, + {JS(close), to_string(change.closeRate.iou())}, + }; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/BookChanges.cpp b/src/rpc/ngHandlers/BookChanges.cpp new file mode 100644 index 00000000..056cbe26 --- /dev/null +++ b/src/rpc/ngHandlers/BookChanges.cpp @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { + +BookChangesHandler::Result +BookChangesHandler::process(BookChangesHandler::Input input, Context const& ctx) const +{ + 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); + auto const transactions = sharedPtrBackend_->fetchAllTransactionsInLedger(lgrInfo.seq, ctx.yield); + + Output response; + response.bookChanges = BookChanges::compute(transactions); + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + response.ledgerTime = lgrInfo.closeTime.time_since_epoch().count(); + return response; +} + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChangesHandler::Output const& output) +{ + jv = { + {JS(type), "bookChanges"}, + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(ledger_time), output.ledgerTime}, + {JS(validated), output.validated}, + {JS(changes), boost::json::value_from(output.bookChanges)}}; +} + +BookChangesHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + BookChangesHandler::Input input; + + 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/BookChanges.h b/src/rpc/ngHandlers/BookChanges.h new file mode 100644 index 00000000..49eebe52 --- /dev/null +++ b/src/rpc/ngHandlers/BookChanges.h @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace RPCng { + +class BookChangesHandler +{ + std::shared_ptr sharedPtrBackend_; + +public: + struct Output + { + std::string ledgerHash; + uint32_t ledgerIndex; + uint32_t ledgerTime; + std::vector bookChanges; + bool validated = true; + }; + + // Clio does not implement deletion_blockers_only + struct Input + { + std::optional ledgerHash; + std::optional ledgerIndex; + }; + + using Result = RPCng::HandlerReturnType; + + BookChangesHandler(std::shared_ptr const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static auto const rpcSpec = RpcSpec{ + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + }; + return rpcSpec; + } + + Result + process(Input input, Context const& ctx) const; + +private: + 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/src/subscriptions/SubscriptionManager.cpp b/src/subscriptions/SubscriptionManager.cpp index e387bf3a..c7d464d6 100644 --- a/src/subscriptions/SubscriptionManager.cpp +++ b/src/subscriptions/SubscriptionManager.cpp @@ -247,6 +247,7 @@ SubscriptionManager::pubBookChanges( ripple::LedgerInfo const& lgrInfo, std::vector const& transactions) { + // TODO: change to ngRPC after old RPC is removed auto const json = RPC::computeBookChanges(lgrInfo, transactions); auto const bookChangesMsg = std::make_shared(boost::json::serialize(json)); bookChangesSubscribers_.publish(bookChangesMsg); diff --git a/unittests/rpc/handlers/BookChangesTest.cpp b/unittests/rpc/handlers/BookChangesTest.cpp new file mode 100644 index 00000000..dfd70b8f --- /dev/null +++ b/unittests/rpc/handlers/BookChangesTest.cpp @@ -0,0 +1,213 @@ +//------------------------------------------------------------------------------ +/* + 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 CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto MAXSEQ = 30; +constexpr static auto MINSEQ = 10; + +class RPCBookChangesHandlerTest : public HandlerBaseTest +{ +}; + +struct BookChangesParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct BookChangesParameterTest : public RPCBookChangesHandlerTest, + 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{ + BookChangesParamTestCaseBundle{ + "LedgerHashInvalid", R"({"ledger_hash":"1"})", "invalidParams", "ledger_hashMalformed"}, + BookChangesParamTestCaseBundle{ + "LedgerHashNotString", R"({"ledger_hash":1})", "invalidParams", "ledger_hashNotString"}, + BookChangesParamTestCaseBundle{ + "LedgerIndexInvalid", R"({"ledger_index":"a"})", "invalidParams", "ledgerIndexMalformed"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCBookChangesGroup1, + BookChangesParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + BookChangesParameterTest::NameGenerator{}); + +TEST_P(BookChangesParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{BookChangesHandler{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(RPCBookChangesHandlerTest, LedgerNonExistViaIntSequence) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // return empty ledgerinfo + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)) + .WillByDefault(Return(std::optional{})); + + auto const static input = boost::json::parse(R"({"ledger_index":30})"); + auto const handler = AnyHandler{BookChangesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, 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(RPCBookChangesHandlerTest, LedgerNonExistViaStringSequence) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // return empty ledgerinfo + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)).WillByDefault(Return(std::nullopt)); + + auto const static input = boost::json::parse(R"({"ledger_index":"30"})"); + auto const handler = AnyHandler{BookChangesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, 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(RPCBookChangesHandlerTest, LedgerNonExistViaHash) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // return empty ledgerinfo + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(std::optional{})); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "ledger_hash":"{}" + }})", + LEDGERHASH)); + auto const handler = AnyHandler{BookChangesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, 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(RPCBookChangesHandlerTest, NormalPath) +{ + static auto constexpr expectedOut = + R"({ + "type":"bookChanges", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "ledger_time":0, + "validated":true, + "changes":[ + { + "currency_a":"XRP_drops", + "currency_b":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD/0158415500000000C1F76FF6ECB0BAC600000000", + "volume_a":"2", + "volume_b":"2", + "high":"-1", + "low":"-1", + "open":"-1", + "close":"-1" + } + ] + })"; + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, MAXSEQ))); + + auto transactions = std::vector{}; + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STObject metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + transactions.push_back(trans1); + + EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger).Times(1); + ON_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(MAXSEQ, _)).WillByDefault(Return(transactions)); + + auto const handler = AnyHandler{BookChangesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(json::parse("{}"), Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOut)); + }); +}