diff --git a/src/rpc/handlers/AccountObjects.cpp b/src/rpc/handlers/AccountObjects.cpp index c5fcca6d..026aae65 100644 --- a/src/rpc/handlers/AccountObjects.cpp +++ b/src/rpc/handlers/AccountObjects.cpp @@ -53,10 +53,43 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input input, Context const if (!accountLedgerObject) return Error{Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + auto typeFilter = std::optional>{}; + + if (input.deletionBlockersOnly) + { + static constexpr ripple::LedgerEntryType deletionBlockers[] = { + ripple::ltCHECK, + ripple::ltESCROW, + ripple::ltNFTOKEN_PAGE, + ripple::ltPAYCHAN, + ripple::ltRIPPLE_STATE, + }; + + typeFilter.emplace(); + typeFilter->reserve(std::size(deletionBlockers)); + + for (auto type : deletionBlockers) + { + if (input.type && input.type != type) + continue; + + typeFilter->push_back(type); + } + } + else + { + if (input.type && input.type != ripple::ltANY) + typeFilter = {*input.type}; + } + Output response; auto const addToResponse = [&](ripple::SLE&& sle) { - if (!input.type || sle.getType() == *(input.type)) + if (not typeFilter or + std::find(std::begin(typeFilter.value()), std::end(typeFilter.value()), sle.getType()) != + std::end(typeFilter.value())) + { response.accountObjects.push_back(std::move(sle)); + } return true; }; @@ -130,6 +163,9 @@ tag_invoke(boost::json::value_to_tag, boost::json: if (jsonObject.contains(JS(marker))) input.marker = jv.at(JS(marker)).as_string().c_str(); + if (jsonObject.contains(JS(deletion_blockers_only))) + input.deletionBlockersOnly = jsonObject.at(JS(deletion_blockers_only)).as_bool(); + return input; } diff --git a/src/rpc/handlers/AccountObjects.h b/src/rpc/handlers/AccountObjects.h index 9a27897e..ef8b8021 100644 --- a/src/rpc/handlers/AccountObjects.h +++ b/src/rpc/handlers/AccountObjects.h @@ -56,7 +56,6 @@ public: bool validated = true; }; - // Clio does not implement deletion_blockers_only struct Input { std::string account; @@ -65,6 +64,7 @@ public: uint32_t limit = 200; // [10,400] std::optional marker; std::optional type; + bool deletionBlockersOnly = false; }; using Result = HandlerReturnType; @@ -97,6 +97,7 @@ public: "nft_offer", }}, {JS(marker), validation::AccountMarkerValidator}, + {JS(deletion_blockers_only), validation::Type{}}, }; return rpcSpec; diff --git a/unittests/rpc/handlers/AccountInfoTest.cpp b/unittests/rpc/handlers/AccountInfoTest.cpp index 419140be..cb3ed2e8 100644 --- a/unittests/rpc/handlers/AccountInfoTest.cpp +++ b/unittests/rpc/handlers/AccountInfoTest.cpp @@ -197,7 +197,7 @@ TEST_F(RPCAccountInfoHandlerTest, LedgerNonExistViaHash) }); } -TEST_F(RPCAccountInfoHandlerTest, AccountNotExsit) +TEST_F(RPCAccountInfoHandlerTest, AccountNotExist) { auto const rawBackendPtr = static_cast(mockBackendPtr.get()); mockBackendPtr->updateRange(10); // min diff --git a/unittests/rpc/handlers/AccountObjectsTest.cpp b/unittests/rpc/handlers/AccountObjectsTest.cpp index c9cef9da..d6d76b59 100644 --- a/unittests/rpc/handlers/AccountObjectsTest.cpp +++ b/unittests/rpc/handlers/AccountObjectsTest.cpp @@ -123,6 +123,16 @@ generateTestValuesForParametersTest() R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker":"xxxx"})", "invalidParams", "Malformed cursor"}, + AccountObjectsParamTestCaseBundle{ + "DeletionBlockersOnlyInvalidString", + R"({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "deletion_blockers_only": "wrong"})", + "invalidParams", + "Invalid parameters."}, + AccountObjectsParamTestCaseBundle{ + "DeletionBlockersOnlyInvalidNull", + R"({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "deletion_blockers_only": null})", + "invalidParams", + "Invalid parameters."}, }; } @@ -224,7 +234,7 @@ TEST_F(RPCAccountObjectsHandlerTest, LedgerNonExistViaHash) }); } -TEST_F(RPCAccountObjectsHandlerTest, AccountNotExsit) +TEST_F(RPCAccountObjectsHandlerTest, AccountNotExist) { auto const rawBackendPtr = static_cast(mockBackendPtr.get()); mockBackendPtr->updateRange(MINSEQ); // min @@ -576,3 +586,229 @@ TEST_F(RPCAccountObjectsHandlerTest, TypeFilterReturnEmpty) EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), 0); }); } + +TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilter) +{ + 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); + + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + auto const channel = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + auto const offer = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + + std::vector bbs; + bbs.push_back(line.getSerializer().peekData()); + bbs.push_back(channel.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": "{}", + "deletion_blockers_only": true + }})", + 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(), 2); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithTypeFilter) +{ + 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); + + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + auto const channel = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + + std::vector bbs; + bbs.push_back(line.getSerializer().peekData()); + bbs.push_back(channel.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": "{}", + "deletion_blockers_only": true, + "type": "payment_channel" + }})", + 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, DeletionBlockersOnlyFilterEmptyResult) +{ + 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); + + auto const offer1 = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + auto const offer2 = CreateOfferLedgerObject( + ACCOUNT, + 20, + 30, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + + std::vector bbs; + bbs.push_back(offer1.getSerializer().peekData()); + bbs.push_back(offer2.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": "{}", + "deletion_blockers_only": true + }})", + 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); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithIncompatibleTypeYieldsEmptyResult) +{ + 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); + + auto const offer1 = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + auto const offer2 = CreateOfferLedgerObject( + ACCOUNT, + 20, + 30, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + + std::vector bbs; + bbs.push_back(offer1.getSerializer().peekData()); + bbs.push_back(offer2.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": "{}", + "deletion_blockers_only": true, + "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(), 0); + }); +}