diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index ffe04b2c..a386adf1 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -17,13 +17,15 @@ */ //============================================================================== -#include #include +#include #include #include #include #include +#include +#include #include #include @@ -405,6 +407,61 @@ getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID) return sle.getFieldU64(ripple::sfOwnerNode); } +// traverse account's nfts +// return Status if error occurs +// return [nextpage, count of nft already found] if success +std::variant +traverseNFTObjects( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& accountID, + ripple::uint256 nextPage, + std::uint32_t limit, + boost::asio::yield_context& yield, + std::function atOwnedNode) +{ + auto const firstNFTPage = ripple::keylet::nftpage_min(accountID); + auto const lastNFTPage = ripple::keylet::nftpage_max(accountID); + + // check if nextPage is valid + if (nextPage != beast::zero and firstNFTPage.key != (nextPage & ~ripple::nft::pageMask)) + return Status{RippledError::rpcINVALID_PARAMS, "Invalid marker."}; + + // no marker, start from the last page + ripple::uint256 currentPage = nextPage == beast::zero ? lastNFTPage.key : nextPage; + + // read the current page + auto page = backend.fetchLedgerObject(currentPage, sequence, yield); + + if (!page) + { + if (nextPage == beast::zero) // no nft objects in lastNFTPage + return AccountCursor{beast::zero, 0}; + else // marker is in the right range, but still invalid + return Status{RippledError::rpcINVALID_PARAMS, "Invalid marker."}; + } + + // the object exists and the key is in right range, must be nft page + ripple::SLE pageSLE{ripple::SLE{ripple::SerialIter{page->data(), page->size()}, currentPage}}; + + auto count = 0; + // traverse the nft page linked list until the start of the list or reach the limit + while (true) + { + auto const nftPreviousPage = pageSLE.getFieldH256(ripple::sfPreviousPageMin); + atOwnedNode(std::move(pageSLE)); + count++; + + if (count == limit or nftPreviousPage == beast::zero) + return AccountCursor{nftPreviousPage, count}; + + page = backend.fetchLedgerObject(nftPreviousPage, sequence, yield); + pageSLE = ripple::SLE{ripple::SerialIter{page->data(), page->size()}, nftPreviousPage}; + } + + return AccountCursor{beast::zero, 0}; +} + std::variant traverseOwnedNodes( BackendInterface const& backend, @@ -413,53 +470,50 @@ traverseOwnedNodes( std::uint32_t limit, std::optional jsonCursor, boost::asio::yield_context& yield, - std::function atOwnedNode) + std::function atOwnedNode, + bool nftIncluded) { - if (!backend.fetchLedgerObject(ripple::keylet::account(accountID).key, sequence, yield)) - return Status{RippledError::rpcACT_NOT_FOUND}; - auto const maybeCursor = parseAccountCursor(jsonCursor); - if (!maybeCursor) - return Status(ripple::rpcINVALID_PARAMS, "Malformed cursor"); + if (!maybeCursor) + return Status{RippledError::rpcINVALID_PARAMS, "Malformed cursor."}; + + // the format is checked in RPC framework level auto [hexCursor, startHint] = *maybeCursor; - return traverseOwnedNodes( - backend, - ripple::keylet::ownerDir(accountID), - hexCursor, - startHint, - sequence, - limit, - jsonCursor, - yield, - atOwnedNode); -} + auto const isNftMarkerNonZero = startHint == std::numeric_limits::max() and hexCursor != beast::zero; + auto const isNftMarkerZero = startHint == std::numeric_limits::max() and hexCursor == beast::zero; + // if we need to traverse nft objects and this is the first request -> traverse nft objects + // if we need to traverse nft objects and the marker is still in nft page -> traverse nft objects + // if we need to traverse nft objects and the marker is still in nft page but next page is zero -> owned nodes + // if we need to traverse nft objects and the marker is not in nft page -> traverse owned nodes + if (nftIncluded and (!jsonCursor or isNftMarkerNonZero)) + { + auto const cursorMaybe = traverseNFTObjects(backend, sequence, accountID, hexCursor, limit, yield, atOwnedNode); -std::variant -ngTraverseOwnedNodes( - BackendInterface const& backend, - ripple::AccountID const& accountID, - std::uint32_t sequence, - std::uint32_t limit, - std::optional jsonCursor, - boost::asio::yield_context& yield, - std::function atOwnedNode) -{ - auto const maybeCursor = parseAccountCursor(jsonCursor); - // the format is checked in RPC framework level - auto const [hexCursor, startHint] = *maybeCursor; + if (auto const status = std::get_if(&cursorMaybe)) + return *status; + + auto const [nextNFTPage, nftsCount] = std::get(cursorMaybe); + + // if limit reach , we return the next page and max as marker + if (nftsCount >= limit) + return AccountCursor{nextNFTPage, std::numeric_limits::max()}; + + // adjust limit ,continue traversing owned nodes + limit -= nftsCount; + hexCursor = beast::zero; + startHint = 0; + } + else if (nftIncluded and isNftMarkerZero) + { + // the last request happen to fetch all the nft, adjust marker to continue traversing owned nodes + hexCursor = beast::zero; + startHint = 0; + } return traverseOwnedNodes( - backend, - ripple::keylet::ownerDir(accountID), - hexCursor, - startHint, - sequence, - limit, - jsonCursor, - yield, - atOwnedNode); + backend, ripple::keylet::ownerDir(accountID), hexCursor, startHint, sequence, limit, yield, atOwnedNode); } std::variant @@ -470,7 +524,6 @@ traverseOwnedNodes( std::uint32_t const startHint, std::uint32_t sequence, std::uint32_t limit, - std::optional jsonCursor, boost::asio::yield_context& yield, std::function atOwnedNode) { @@ -496,7 +549,7 @@ traverseOwnedNodes( auto hintDir = backend.fetchLedgerObject(hintIndex.key, sequence, yield); if (!hintDir) - return Status(ripple::rpcINVALID_PARAMS, "Invalid marker"); + return Status(ripple::rpcINVALID_PARAMS, "Invalid marker."); ripple::SerialIter it{hintDir->data(), hintDir->size()}; ripple::SLE sle{it, hintIndex.key}; @@ -505,7 +558,7 @@ traverseOwnedNodes( std::find(std::begin(indexes), std::end(indexes), hexMarker) == std::end(indexes)) { // the index specified by marker is not in the page specified by marker - return Status(ripple::rpcINVALID_PARAMS, "Invalid marker"); + return Status(ripple::rpcINVALID_PARAMS, "Invalid marker."); } currentIndex = hintIndex; @@ -515,7 +568,7 @@ traverseOwnedNodes( auto const ownerDir = backend.fetchLedgerObject(currentIndex.key, sequence, yield); if (!ownerDir) - return Status(ripple::rpcINVALID_PARAMS, "Owner directory not found"); + return Status(ripple::rpcINVALID_PARAMS, "Owner directory not found."); ripple::SerialIter it{ownerDir->data(), ownerDir->size()}; ripple::SLE sle{it, currentIndex.key}; diff --git a/src/rpc/RPCHelpers.h b/src/rpc/RPCHelpers.h index f237e5c2..fce1fa7d 100644 --- a/src/rpc/RPCHelpers.h +++ b/src/rpc/RPCHelpers.h @@ -101,16 +101,6 @@ getLedgerInfoFromHashOrSeq( std::optional ledgerIndex, uint32_t maxSeq); -std::variant -traverseOwnedNodes( - BackendInterface const& backend, - ripple::AccountID const& accountID, - std::uint32_t sequence, - std::uint32_t limit, - std::optional jsonCursor, - boost::asio::yield_context& yield, - std::function atOwnedNode); - std::variant traverseOwnedNodes( BackendInterface const& backend, @@ -119,21 +109,21 @@ traverseOwnedNodes( std::uint32_t const startHint, std::uint32_t sequence, std::uint32_t limit, - std::optional jsonCursor, boost::asio::yield_context& yield, std::function atOwnedNode); // Remove the account check from traverseOwnedNodes // Account check has been done by framework,remove it from internal function std::variant -ngTraverseOwnedNodes( +traverseOwnedNodes( BackendInterface const& backend, ripple::AccountID const& accountID, std::uint32_t sequence, std::uint32_t limit, std::optional jsonCursor, boost::asio::yield_context& yield, - std::function atOwnedNode); + std::function atOwnedNode, + bool nftIncluded = false); std::shared_ptr read( diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index 4d5c51bc..c9ab2df2 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -160,7 +160,7 @@ CustomValidator AccountMarkerValidator = if (!parseAccountCursor(value.as_string().c_str())) { // align with the current error message - return Error{Status{RippledError::rpcINVALID_PARAMS, "Malformed cursor"}}; + return Error{Status{RippledError::rpcINVALID_PARAMS, "Malformed cursor."}}; } return MaybeError{}; diff --git a/src/rpc/handlers/AccountChannels.cpp b/src/rpc/handlers/AccountChannels.cpp index f5789a93..35a8b598 100644 --- a/src/rpc/handlers/AccountChannels.cpp +++ b/src/rpc/handlers/AccountChannels.cpp @@ -87,7 +87,7 @@ AccountChannelsHandler::process(AccountChannelsHandler::Input input, Context con return true; }; - auto const next = ngTraverseOwnedNodes( + auto const next = traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse); if (auto status = std::get_if(&next)) diff --git a/src/rpc/handlers/AccountCurrencies.cpp b/src/rpc/handlers/AccountCurrencies.cpp index f1094a65..4ee0e93f 100644 --- a/src/rpc/handlers/AccountCurrencies.cpp +++ b/src/rpc/handlers/AccountCurrencies.cpp @@ -63,7 +63,7 @@ AccountCurrenciesHandler::process(AccountCurrenciesHandler::Input input, Context }; // traverse all owned nodes, limit->max, marker->empty - ngTraverseOwnedNodes( + traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, diff --git a/src/rpc/handlers/AccountLines.cpp b/src/rpc/handlers/AccountLines.cpp index 0a445715..2ed59060 100644 --- a/src/rpc/handlers/AccountLines.cpp +++ b/src/rpc/handlers/AccountLines.cpp @@ -129,7 +129,7 @@ AccountLinesHandler::process(AccountLinesHandler::Input input, Context const& ct } }; - auto const next = ngTraverseOwnedNodes( + auto const next = traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse); if (auto status = std::get_if(&next)) diff --git a/src/rpc/handlers/AccountObjects.cpp b/src/rpc/handlers/AccountObjects.cpp index 026aae65..55504f20 100644 --- a/src/rpc/handlers/AccountObjects.cpp +++ b/src/rpc/handlers/AccountObjects.cpp @@ -21,7 +21,6 @@ namespace RPC { -// document does not mention nft_page, we still support it tho std::unordered_map const AccountObjectsHandler::TYPESMAP{ {"state", ripple::ltRIPPLE_STATE}, {"ticket", ripple::ltTICKET}, @@ -93,8 +92,8 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input input, Context const return true; }; - auto const next = ngTraverseOwnedNodes( - *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse); + auto const next = traverseOwnedNodes( + *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse, true); if (auto status = std::get_if(&next)) return Error{*status}; diff --git a/src/rpc/handlers/AccountOffers.cpp b/src/rpc/handlers/AccountOffers.cpp index 2a9bbb6d..837c27e1 100644 --- a/src/rpc/handlers/AccountOffers.cpp +++ b/src/rpc/handlers/AccountOffers.cpp @@ -70,7 +70,7 @@ AccountOffersHandler::process(AccountOffersHandler::Input input, Context const& return true; }; - auto const next = ngTraverseOwnedNodes( + auto const next = traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse); if (auto const status = std::get_if(&next)) diff --git a/src/rpc/handlers/GatewayBalances.cpp b/src/rpc/handlers/GatewayBalances.cpp index a3aaf5f3..62c16d94 100644 --- a/src/rpc/handlers/GatewayBalances.cpp +++ b/src/rpc/handlers/GatewayBalances.cpp @@ -109,7 +109,7 @@ GatewayBalancesHandler::process(GatewayBalancesHandler::Input input, Context con }; // traverse all owned nodes, limit->max, marker->empty - auto const ret = ngTraverseOwnedNodes( + auto const ret = traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, diff --git a/src/rpc/handlers/NFTOffersCommon.cpp b/src/rpc/handlers/NFTOffersCommon.cpp index af69e418..6f5afde9 100644 --- a/src/rpc/handlers/NFTOffersCommon.cpp +++ b/src/rpc/handlers/NFTOffersCommon.cpp @@ -111,15 +111,7 @@ NFTOffersHandlerBase::iterateOfferDirectory( } auto result = traverseOwnedNodes( - *sharedPtrBackend_, - directory, - cursor, - startHint, - lgrInfo.seq, - reserve, - {}, - yield, - [&offers](ripple::SLE&& offer) { + *sharedPtrBackend_, directory, cursor, startHint, lgrInfo.seq, reserve, yield, [&offers](ripple::SLE&& offer) { if (offer.getType() == ripple::ltNFTOKEN_OFFER) { offers.push_back(std::move(offer)); diff --git a/src/rpc/handlers/NoRippleCheck.cpp b/src/rpc/handlers/NoRippleCheck.cpp index 7ea054ce..8f486ebd 100644 --- a/src/rpc/handlers/NoRippleCheck.cpp +++ b/src/rpc/handlers/NoRippleCheck.cpp @@ -85,7 +85,7 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input input, Context const& auto limit = input.limit; - ngTraverseOwnedNodes( + traverseOwnedNodes( *sharedPtrBackend_, *accountID, lgrInfo.seq, diff --git a/unittests/rpc/RPCHelpersTest.cpp b/unittests/rpc/RPCHelpersTest.cpp index 8ecb6c8e..fbea8394 100644 --- a/unittests/rpc/RPCHelpersTest.cpp +++ b/unittests/rpc/RPCHelpersTest.cpp @@ -50,33 +50,8 @@ class RPCHelpersTest : public MockBackendTest, public SyncAsioContextTest } }; -TEST_F(RPCHelpersTest, TraverseOwnedNodesNotAccount) -{ - MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); - // fetch account object return emtpy - ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(std::optional{})); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); - - boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { - auto account = GetAccountIDWithString(ACCOUNT); - auto ret = traverseOwnedNodes(*mockBackendPtr, account, 9, 10, "", yield, [](auto) { - - }); - auto status = std::get_if(&ret); - EXPECT_TRUE(status != nullptr); - EXPECT_EQ(*status, RippledError::rpcACT_NOT_FOUND); - }); - ctx.run(); -} - TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidIndexNotHex) { - MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); - boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto account = GetAccountIDWithString(ACCOUNT); auto ret = traverseOwnedNodes(*mockBackendPtr, account, 9, 10, "nothex,10", yield, [](auto) { @@ -85,19 +60,13 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidIndexNotHex) auto status = std::get_if(&ret); EXPECT_TRUE(status != nullptr); EXPECT_EQ(*status, ripple::rpcINVALID_PARAMS); - EXPECT_EQ(status->message, "Malformed cursor"); + EXPECT_EQ(status->message, "Malformed cursor."); }); ctx.run(); } TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidPageNotInt) { - MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); - boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto account = GetAccountIDWithString(ACCOUNT); auto ret = traverseOwnedNodes(*mockBackendPtr, account, 9, 10, "nothex,abc", yield, [](auto) { @@ -106,7 +75,7 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidPageNotInt) auto status = std::get_if(&ret); EXPECT_TRUE(status != nullptr); EXPECT_EQ(*status, ripple::rpcINVALID_PARAMS); - EXPECT_EQ(status->message, "Malformed cursor"); + EXPECT_EQ(status->message, "Malformed cursor."); }); ctx.run(); } @@ -117,12 +86,8 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarker) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); auto account = GetAccountIDWithString(ACCOUNT); - auto accountKk = ripple::keylet::account(account).key; auto owneDirKk = ripple::keylet::ownerDir(account).key; - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); // return owner index ripple::STObject ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); @@ -157,12 +122,8 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarkerReturnSamePageMarker) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); auto account = GetAccountIDWithString(ACCOUNT); - auto accountKk = ripple::keylet::account(account).key; auto owneDirKk = ripple::keylet::ownerDir(account).key; - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); std::vector bbs; @@ -202,16 +163,12 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarkerReturnOtherPageMarker) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); auto account = GetAccountIDWithString(ACCOUNT); - auto accountKk = ripple::keylet::account(account).key; auto ownerDirKk = ripple::keylet::ownerDir(account).key; constexpr static auto nextPage = 99; constexpr static auto limit = 15; auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), nextPage).key; - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); std::vector bbs; @@ -262,14 +219,10 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesWithMarkerReturnSamePageMarker) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); auto account = GetAccountIDWithString(ACCOUNT); - auto accountKk = ripple::keylet::account(account).key; auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), 99).key; constexpr static auto limit = 8; constexpr static auto pageNum = 99; - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); std::vector bbs; @@ -317,14 +270,10 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesWithUnexistingIndexMarker) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); auto account = GetAccountIDWithString(ACCOUNT); - auto accountKk = ripple::keylet::account(account).key; auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), 99).key; constexpr static auto limit = 8; constexpr static auto pageNum = 99; - // fetch account object return something - auto fake = Blob{'f', 'a', 'k', 'e'}; - ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)).WillByDefault(Return(fake)); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); int objectsCount = 10; ripple::STObject channel1 = CreatePaymentChannelLedgerObject(ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); @@ -348,7 +297,7 @@ TEST_F(RPCHelpersTest, TraverseOwnedNodesWithUnexistingIndexMarker) auto status = std::get_if(&ret); EXPECT_TRUE(status != nullptr); EXPECT_EQ(*status, ripple::rpcINVALID_PARAMS); - EXPECT_EQ(status->message, "Invalid marker"); + EXPECT_EQ(status->message, "Invalid marker."); }); ctx.run(); } diff --git a/unittests/rpc/handlers/AccountChannelsTest.cpp b/unittests/rpc/handlers/AccountChannelsTest.cpp index ffb921a7..86175f05 100644 --- a/unittests/rpc/handlers/AccountChannelsTest.cpp +++ b/unittests/rpc/handlers/AccountChannelsTest.cpp @@ -137,7 +137,7 @@ TEST_F(RPCAccountHandlerTest, InvalidMarker) auto const err = RPC::makeError(output.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor"); + EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor."); }); runSpawn([&, this](auto& yield) { auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; diff --git a/unittests/rpc/handlers/AccountLinesTest.cpp b/unittests/rpc/handlers/AccountLinesTest.cpp index e4ce7ced..29d8103d 100644 --- a/unittests/rpc/handlers/AccountLinesTest.cpp +++ b/unittests/rpc/handlers/AccountLinesTest.cpp @@ -142,7 +142,7 @@ TEST_F(RPCAccountLinesHandlerTest, InvalidMarker) auto const err = RPC::makeError(output.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor"); + EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor."); }); runSpawn([this](auto& yield) { auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; diff --git a/unittests/rpc/handlers/AccountObjectsTest.cpp b/unittests/rpc/handlers/AccountObjectsTest.cpp index d6d76b59..f007b369 100644 --- a/unittests/rpc/handlers/AccountObjectsTest.cpp +++ b/unittests/rpc/handlers/AccountObjectsTest.cpp @@ -24,6 +24,8 @@ #include +#include + using namespace RPC; namespace json = boost::json; using namespace testing; @@ -34,6 +36,7 @@ constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; constexpr static auto TXNID = "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"; +constexpr static auto TOKENID = "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA"; constexpr static auto MAXSEQ = 30; constexpr static auto MINSEQ = 10; @@ -122,7 +125,14 @@ generateTestValuesForParametersTest() "MarkerInvalid", R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker":"xxxx"})", "invalidParams", - "Malformed cursor"}, + "Malformed cursor."}, + AccountObjectsParamTestCaseBundle{ + "NFTMarkerInvalid", + fmt::format( + R"({{"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "marker":"wronghex256,{}"}})", + std::numeric_limits::max()), + "invalidParams", + "Malformed cursor."}, AccountObjectsParamTestCaseBundle{ "DeletionBlockersOnlyInvalidString", R"({"account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "deletion_blockers_only": "wrong"})", @@ -261,7 +271,7 @@ TEST_F(RPCAccountObjectsHandlerTest, AccountNotExist) }); } -TEST_F(RPCAccountObjectsHandlerTest, DefaultParameter) +TEST_F(RPCAccountObjectsHandlerTest, DefaultParameterNoNFTFound) { static auto constexpr expectedOut = R"({ "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", @@ -302,14 +312,20 @@ TEST_F(RPCAccountObjectsHandlerTest, DefaultParameter) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); std::vector bbs; auto const line1 = CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); @@ -341,17 +357,23 @@ TEST_F(RPCAccountObjectsHandlerTest, Limit) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); std::vector bbs; while (count-- != 0) @@ -430,7 +452,7 @@ TEST_F(RPCAccountObjectsHandlerTest, Marker) }); } -TEST_F(RPCAccountObjectsHandlerTest, MultipleDir) +TEST_F(RPCAccountObjectsHandlerTest, MultipleDirNoNFT) { auto const rawBackendPtr = static_cast(mockBackendPtr.get()); mockBackendPtr->updateRange(MINSEQ); // min @@ -439,7 +461,8 @@ TEST_F(RPCAccountObjectsHandlerTest, MultipleDir) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); static auto constexpr count = 10; @@ -448,13 +471,18 @@ TEST_F(RPCAccountObjectsHandlerTest, MultipleDir) 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 ownerDirKk = ripple::keylet::ownerDir(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); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(4); std::vector bbs; // 10 items per page, 2 pages @@ -494,14 +522,19 @@ TEST_F(RPCAccountObjectsHandlerTest, TypeFilter) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); std::vector bbs; // put 1 state and 1 offer @@ -546,14 +579,20 @@ TEST_F(RPCAccountObjectsHandlerTest, TypeFilterReturnEmpty) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + std::vector bbs; auto const line1 = CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); @@ -598,14 +637,21 @@ TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilter) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); auto const line = CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); @@ -654,14 +700,19 @@ TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithTypeFilter) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); auto const line = CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); @@ -701,14 +752,20 @@ TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterEmptyResult) EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); auto const offer1 = CreateOfferLedgerObject( ACCOUNT, @@ -762,14 +819,18 @@ TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithIncompatibleT EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); - auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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; + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) .WillByDefault(Return(ownerDir.getSerializer().peekData())); - EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + // nft null + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)).WillByDefault(Return(std::nullopt)); auto const offer1 = CreateOfferLedgerObject( ACCOUNT, @@ -812,3 +873,666 @@ TEST_F(RPCAccountObjectsHandlerTest, DeletionBlockersOnlyFilterWithIncompatibleT EXPECT_EQ(output->as_object().at("account_objects").as_array().size(), 0); }); } + +TEST_F(RPCAccountObjectsHandlerTest, NFTMixOtherObjects) +{ + static auto constexpr expectedOut = R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "limit": 200, + "account_objects":[ + { + "Flags":0, + "LedgerEntryType":"NFTokenPage", + "NFTokens":[ + { + "NFToken":{ + "NFTokenID":"000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", + "URI":"7777772E6F6B2E636F6D" + } + } + ], + "PreviousPageMin":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC", + "PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq":0, + "index":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9FFFFFFFFFFFFFFFFFFFFFFFF" + }, + { + "Flags":0, + "LedgerEntryType":"NFTokenPage", + "NFTokens":[ + { + "NFToken":{ + "NFTokenID":"000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", + "URI":"7777772E6F6B2E636F6D" + } + } + ], + "PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq":0, + "index":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC" + }, + { + "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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + // nft page 1 + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + auto const nftPage2KK = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{INDEX1}).key; + auto const nftpage1 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, nftPage2KK); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)) + .WillByDefault(Return(nftpage1.getSerializer().peekData())); + + // nft page 2 , end + auto const nftpage2 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, std::nullopt); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftPage2KK, 30, _)) + .WillByDefault(Return(nftpage2.getSerializer().peekData())); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(4); + 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, NFTReachLimitReturnMarker) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto current = ripple::keylet::nftpage_max(account).key; + std::string first{INDEX1}; + sort(first.begin(), first.end()); + for (auto i = 0; i < 10; i++) + { + std::next_permutation(first.begin(), first.end()); + auto previous = + ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + auto const nftpage = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, previous); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage.getSerializer().peekData())); + current = previous; + } + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(11); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "limit":{} + }})", + ACCOUNT, + 10)); + + 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.value().as_object().at("account_objects").as_array().size(), 10); + EXPECT_EQ( + output.value().as_object().at("marker").as_string(), + fmt::format("{},{}", ripple::strHex(current), std::numeric_limits::max())); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, NFTReachLimitNoMarker) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto current = ripple::keylet::nftpage_max(account).key; + std::string first{INDEX1}; + sort(first.begin(), first.end()); + for (auto i = 0; i < 10; i++) + { + std::next_permutation(first.begin(), first.end()); + auto previous = + ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + auto const nftpage = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, previous); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage.getSerializer().peekData())); + current = previous; + } + auto const nftpage11 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, std::nullopt); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage11.getSerializer().peekData())); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(12); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "limit":{} + }})", + ACCOUNT, + 11)); + + 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.value().as_object().at("account_objects").as_array().size(), 11); + //"0000000000000000000000000000000000000000000000000000000000000000,4294967295" + EXPECT_EQ( + output.value().as_object().at("marker").as_string(), + fmt::format("{},{}", ripple::strHex(ripple::uint256(beast::zero)), std::numeric_limits::max())); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, NFTMarker) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + std::string first{INDEX1}; + auto current = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + const auto marker = current; + sort(first.begin(), first.end()); + for (auto i = 0; i < 10; i++) + { + std::next_permutation(first.begin(), first.end()); + auto previous = + ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + auto const nftpage = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, previous); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage.getSerializer().peekData())); + current = previous; + } + auto const nftpage11 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, std::nullopt); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage11.getSerializer().peekData())); + + auto const ownerDir = + CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}, ripple::uint256{INDEX1}, ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + 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); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(13); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "marker":"{},{}" + }})", + ACCOUNT, + ripple::strHex(marker), + std::numeric_limits::max())); + + 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.value().as_object().at("account_objects").as_array().size(), 11 + 3); + EXPECT_FALSE(output.value().as_object().contains("marker")); + }); +} + +// when limit reached, happen to be the end of NFT page list +TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNoMoreNFT) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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}, ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + 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); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "marker":"{},{}" + }})", + ACCOUNT, + ripple::strHex(ripple::uint256{beast::zero}), + std::numeric_limits::max())); + + 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.value().as_object().at("account_objects").as_array().size(), 3); + EXPECT_FALSE(output.value().as_object().contains("marker")); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNotInRange) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "marker" : "{},{}" + }})", + ACCOUNT, + INDEX1, + std::numeric_limits::max())); + + 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(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Invalid marker."); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, NFTMarkerNotExist) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + // return null for this marker + auto const accountNftMax = ripple::keylet::nftpage_max(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountNftMax, MAXSEQ, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "marker" : "{},{}" + }})", + ACCOUNT, + ripple::strHex(accountNftMax), + std::numeric_limits::max())); + + 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(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Invalid marker."); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, NFTLimitAdjust) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + std::string first{INDEX1}; + auto current = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + const auto marker = current; + sort(first.begin(), first.end()); + for (auto i = 0; i < 10; i++) + { + std::next_permutation(first.begin(), first.end()); + auto previous = + ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{first.c_str()}).key; + auto const nftpage = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, previous); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage.getSerializer().peekData())); + current = previous; + } + auto const nftpage11 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, std::nullopt); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(current, 30, _)) + .WillByDefault(Return(nftpage11.getSerializer().peekData())); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}, ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + 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); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(13); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "marker":"{},{}", + "limit": 12 + }})", + ACCOUNT, + ripple::strHex(marker), + std::numeric_limits::max())); + + 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.value().as_object().at("account_objects").as_array().size(), 12); + // marker not in NFT "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC,0" + EXPECT_EQ(output.value().as_object().at("marker").as_string(), fmt::format("{},{}", INDEX1, 0)); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, FilterNFT) +{ + static auto constexpr expectedOut = R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "limit": 200, + "account_objects":[ + { + "Flags":0, + "LedgerEntryType":"NFTokenPage", + "NFTokens":[ + { + "NFToken":{ + "NFTokenID":"000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", + "URI":"7777772E6F6B2E636F6D" + } + } + ], + "PreviousPageMin":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC", + "PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq":0, + "index":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9FFFFFFFFFFFFFFFFFFFFFFFF" + }, + { + "Flags":0, + "LedgerEntryType":"NFTokenPage", + "NFTokens":[ + { + "NFToken":{ + "NFTokenID":"000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA", + "URI":"7777772E6F6B2E636F6D" + } + } + ], + "PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq":0, + "index":"4B4E9C06F24296074F7BC48F92A97916C6DC5EA9659B25014D08E1BC983515BC" + } + ] + })"; + + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + // nft page 1 + auto const nftMaxKK = ripple::keylet::nftpage_max(account).key; + auto const nftPage2KK = ripple::keylet::nftpage(ripple::keylet::nftpage_min(account), ripple::uint256{INDEX1}).key; + auto const nftpage1 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, nftPage2KK); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftMaxKK, 30, _)) + .WillByDefault(Return(nftpage1.getSerializer().peekData())); + + // nft page 2 , end + auto const nftpage2 = + CreateNFTTokenPage(std::vector{std::make_pair(TOKENID, "www.ok.com")}, std::nullopt); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(nftPage2KK, 30, _)) + .WillByDefault(Return(nftpage2.getSerializer().peekData())); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(4); + 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":"{}", + "type": "nft_page" + }})", + 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, NFTZeroMarkerNotAffectOtherMarker) +{ + 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 account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(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(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":{}, + "marker": "{},{}" + }})", + ACCOUNT, + limit, + ripple::strHex(ripple::uint256{beast::zero}), + std::numeric_limits::max())); + + 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)); + }); +} diff --git a/unittests/rpc/handlers/AccountOffersTest.cpp b/unittests/rpc/handlers/AccountOffersTest.cpp index aa9c28c5..3bcde298 100644 --- a/unittests/rpc/handlers/AccountOffersTest.cpp +++ b/unittests/rpc/handlers/AccountOffersTest.cpp @@ -129,7 +129,7 @@ generateTestValuesForParametersTest() "MarkerInvalid", R"({"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "marker": "12;xxx"})", "invalidParams", - "Malformed cursor", + "Malformed cursor.", }, { "StrictFieldUnsupportedValue", @@ -589,6 +589,6 @@ TEST_F(RPCAccountOffersHandlerTest, MarkerNotExists) 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(), "Invalid marker"); + EXPECT_EQ(err.at("error_message").as_string(), "Invalid marker."); }); }