From 4b2d53fc2fab0c00ad2e1f0ed6a6f5a5e1d5adac Mon Sep 17 00:00:00 2001 From: cyan317 <120398799+cindyyan317@users.noreply.github.com> Date: Tue, 25 Apr 2023 09:14:20 +0100 Subject: [PATCH] account_objects of new RPC framework (#599) Fixes #602 --- CMakeLists.txt | 2 + src/rpc/ngHandlers/AccountObjects.cpp | 130 ++++ src/rpc/ngHandlers/AccountObjects.h | 103 ++++ unittests/rpc/handlers/AccountObjectsTest.cpp | 578 ++++++++++++++++++ 4 files changed, 813 insertions(+) create mode 100644 src/rpc/ngHandlers/AccountObjects.cpp create mode 100644 src/rpc/ngHandlers/AccountObjects.h create mode 100644 unittests/rpc/handlers/AccountObjectsTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 899c315a..47336690 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/NFTHistory.cpp src/rpc/ngHandlers/LedgerData.cpp src/rpc/ngHandlers/AccountNFTs.cpp + src/rpc/ngHandlers/AccountObjects.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -175,6 +176,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/SubscribeTest.cpp unittests/rpc/handlers/UnsubscribeTest.cpp unittests/rpc/handlers/LedgerDataTest.cpp + unittests/rpc/handlers/AccountObjectsTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/ngHandlers/AccountObjects.cpp b/src/rpc/ngHandlers/AccountObjects.cpp new file mode 100644 index 00000000..c68842bc --- /dev/null +++ b/src/rpc/ngHandlers/AccountObjects.cpp @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +// document does not mention nft_page, we still support it tho +std::unordered_map const AccountObjectsHandler::TYPESMAP{ + {"state", ripple::ltRIPPLE_STATE}, + {"ticket", ripple::ltTICKET}, + {"signer_list", ripple::ltSIGNER_LIST}, + {"payment_channel", ripple::ltPAYCHAN}, + {"offer", ripple::ltOFFER}, + {"escrow", ripple::ltESCROW}, + {"deposit_preauth", ripple::ltDEPOSIT_PREAUTH}, + {"check", ripple::ltCHECK}, + {"nft_page", ripple::ltNFTOKEN_PAGE}, + {"nft_offer", ripple::ltNFTOKEN_OFFER}}; + +AccountObjectsHandler::Result +AccountObjectsHandler::process(AccountObjectsHandler::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 accountID = RPC::accountFromStringStrict(input.account); + auto const accountLedgerObject = + sharedPtrBackend_->fetchLedgerObject(ripple::keylet::account(*accountID).key, lgrInfo.seq, ctx.yield); + + if (!accountLedgerObject) + return Error{RPC::Status{RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + + Output response; + auto const addToResponse = [&](ripple::SLE&& sle) { + if (!input.type || sle.getType() == *(input.type)) + response.accountObjects.push_back(std::move(sle)); + return true; + }; + + auto const next = RPC::ngTraverseOwnedNodes( + *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse); + + if (auto status = std::get_if(&next)) + return Error{*status}; + + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + response.limit = input.limit; + response.account = input.account; + + auto const& nextMarker = std::get(next); + + if (nextMarker.isNonZero()) + response.marker = nextMarker.toString(); + + return response; +} + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountObjectsHandler::Output const& output) +{ + boost::json::array objects; + for (auto const& sle : output.accountObjects) + objects.push_back(RPC::toJson(sle)); + + jv = { + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(validated), output.validated}, + {JS(limit), output.limit}, + {JS(account), output.account}, + {JS(account_objects), objects}}; + + if (output.marker) + jv.as_object()[JS(marker)] = *(output.marker); +} + +AccountObjectsHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + AccountObjectsHandler::Input input; + input.account = jv.at(JS(account)).as_string().c_str(); + + 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()); + } + + if (jsonObject.contains(JS(type))) + input.type = AccountObjectsHandler::TYPESMAP.at(jv.at(JS(type)).as_string().c_str()); + + if (jsonObject.contains(JS(limit))) + input.limit = jv.at(JS(limit)).as_int64(); + + if (jsonObject.contains(JS(marker))) + input.marker = jv.at(JS(marker)).as_string().c_str(); + + return input; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountObjects.h b/src/rpc/ngHandlers/AccountObjects.h new file mode 100644 index 00000000..0f6b1e11 --- /dev/null +++ b/src/rpc/ngHandlers/AccountObjects.h @@ -0,0 +1,103 @@ +//------------------------------------------------------------------------------ +/* + 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 AccountObjectsHandler +{ + // dependencies + std::shared_ptr sharedPtrBackend_; + static std::unordered_map const TYPESMAP; + +public: + struct Output + { + std::string account; + std::string ledgerHash; + uint32_t ledgerIndex; + std::optional marker; + uint32_t limit; + std::vector accountObjects; + bool validated = true; + }; + + // Clio does not implement deletion_blockers_only + struct Input + { + std::string account; + std::optional ledgerHash; + std::optional ledgerIndex; + uint32_t limit = 200; //[10,400] + std::optional marker; + std::optional type; + }; + + using Result = RPCng::HandlerReturnType; + + AccountObjectsHandler(std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static auto const rpcSpec = RpcSpec{ + {JS(account), validation::Required{}, validation::AccountValidator}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(limit), validation::Type{}, validation::Between(10, 400)}, + {JS(type), + validation::Type{}, + validation::OneOf{ + "state", + "ticket", + "signer_list", + "payment_channel", + "offer", + "escrow", + "deposit_preauth", + "check", + "nft_page", + "nft_offer"}}, + {JS(marker), validation::AccountMarkerValidator}, + }; + + 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/unittests/rpc/handlers/AccountObjectsTest.cpp b/unittests/rpc/handlers/AccountObjectsTest.cpp new file mode 100644 index 00000000..eeabc3c0 --- /dev/null +++ b/unittests/rpc/handlers/AccountObjectsTest.cpp @@ -0,0 +1,578 @@ +//------------------------------------------------------------------------------ +/* + 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 ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ISSUER = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto TXNID = "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"; +constexpr static auto MAXSEQ = 30; +constexpr static auto MINSEQ = 10; + +class RPCAccountObjectsHandlerTest : public HandlerBaseTest +{ +}; + +struct AccountObjectsParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct AccountObjectsParameterTest : public RPCAccountObjectsHandlerTest, + 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{ + AccountObjectsParamTestCaseBundle{ + "MissingAccount", R"({})", "invalidParams", "Required field 'account' missing"}, + AccountObjectsParamTestCaseBundle{"AccountNotString", R"({"account":1})", "invalidParams", "accountNotString"}, + AccountObjectsParamTestCaseBundle{"AccountInvalid", R"({"account":"xxx"})", "actMalformed", "accountMalformed"}, + AccountObjectsParamTestCaseBundle{ + "TypeNotString", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type":1})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "TypeInvalid", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type":"wrong"})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "LedgerHashInvalid", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":"1"})", + "invalidParams", + "ledger_hashMalformed"}, + AccountObjectsParamTestCaseBundle{ + "LedgerHashNotString", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":1})", + "invalidParams", + "ledger_hashNotString"}, + AccountObjectsParamTestCaseBundle{ + "LedgerIndexInvalid", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_index":"a"})", + "invalidParams", + "ledgerIndexMalformed"}, + AccountObjectsParamTestCaseBundle{ + "LimitNotInt", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit":"1"})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "LimitLargerThanMax", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit":401})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "LimitLessThanMin", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "limit":9})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "MarkerNotString", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker":9})", + "invalidParams", + "markerNotString"}, + AccountObjectsParamTestCaseBundle{ + "MarkerInvalid", + R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker":"xxxx"})", + "invalidParams", + "Malformed cursor"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCAccountObjectsGroup1, + AccountObjectsParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + AccountObjectsParameterTest::NameGenerator{}); + +TEST_P(AccountObjectsParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountObjectsHandler{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(RPCAccountObjectsHandlerTest, 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(fmt::format( + R"({{ + "account":"{}", + "ledger_index":30 + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountObjectsHandler{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(RPCAccountObjectsHandlerTest, 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(fmt::format( + R"({{ + "account":"{}", + "ledger_index":"30" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountObjectsHandler{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(RPCAccountObjectsHandlerTest, 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"({{ + "account":"{}", + "ledger_hash":"{}" + }})", + ACCOUNT, + LEDGERHASH)); + auto const handler = AnyHandler{AccountObjectsHandler{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(RPCAccountObjectsHandlerTest, AccountNotExsit) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountObjectsHandler{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(), "actNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound"); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, DefaultParameter) +{ + static auto constexpr expectedOut = R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "limit": 200, + "account_objects":[ + { + "Balance":{ + "currency":"USD", + "issuer":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + "value":"100" + }, + "Flags":0, + "HighLimit":{ + "currency":"USD", + "issuer":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value":"20" + }, + "LedgerEntryType":"RippleState", + "LowLimit":{ + "currency":"USD", + "issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value":"10" + }, + "PreviousTxnID":"E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879", + "PreviousTxnLgrSeq":123, + "index":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" + } + ] + })"; + + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOut)); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, Limit) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + static auto constexpr limit = 10; + auto count = limit * 2; + // put 20 items in owner dir, but only return 10 + auto const ownerDir = CreateOwnerDirLedgerObject(std::vector(count, ripple::uint256{INDEX1}), INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + std::vector bbs; + while (count-- != 0) + { + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + } + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "limit":{} + }})", + ACCOUNT, + limit)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), limit); + EXPECT_EQ(output->as_object().at("marker").as_string(), fmt::format("{},{}", INDEX1, 0)); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, Marker) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + static auto constexpr limit = 20; + static auto constexpr page = 2; + auto count = limit; + auto const ownerDir = CreateOwnerDirLedgerObject(std::vector(count, ripple::uint256{INDEX1}), INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + auto const hintIndex = ripple::keylet::page(ownerDirKk, page).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(hintIndex, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + while (count-- != 0) + { + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + } + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "marker":"{},{}" + }})", + ACCOUNT, + INDEX1, + page)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), limit - 1); + EXPECT_FALSE(output->as_object().contains("marker")); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, MultipleDir) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + static auto constexpr count = 10; + static auto constexpr nextpage = 1; + auto cc = count; + auto ownerDir = CreateOwnerDirLedgerObject(std::vector(cc, ripple::uint256{INDEX1}), INDEX1); + // set next page + ownerDir.setFieldU64(ripple::sfIndexNext, nextpage); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + auto const page1 = ripple::keylet::page(ownerDirKk, nextpage).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(page1, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + // 10 items per page, 2 pages + cc = count * 2; + while (cc-- != 0) + { + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + } + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "limit":{} + }})", + ACCOUNT, + 2 * count)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), count * 2); + EXPECT_EQ(output->as_object().at("marker").as_string(), fmt::format("{},{}", INDEX1, nextpage)); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, TypeFilter) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}, ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + std::vector bbs; + // put 1 state and 1 offer + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + auto const offer = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(offer.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "type":"offer" + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), 1); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, TypeFilterReturnEmpty) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}, ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + auto const offer = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(offer.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "type": "check" + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountObjectsHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), 0); + }); +}