account_object supports nft page (#736)

Fix #696
This commit is contained in:
cyan317
2023-07-10 13:42:57 +01:00
committed by GitHub
parent 7b306f3ba0
commit 271323b0f4
16 changed files with 876 additions and 169 deletions

View File

@@ -17,13 +17,15 @@
*/
//==============================================================================
#include <ripple/basics/StringUtilities.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <log/Logger.h>
#include <rpc/Errors.h>
#include <rpc/RPCHelpers.h>
#include <util/Profiler.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/nftPageMask.h>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
@@ -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<Status, AccountCursor>
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<void(ripple::SLE&&)> 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<Status, AccountCursor>
traverseOwnedNodes(
BackendInterface const& backend,
@@ -413,53 +470,50 @@ traverseOwnedNodes(
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> atOwnedNode)
std::function<void(ripple::SLE&&)> 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<uint32_t>::max() and hexCursor != beast::zero;
auto const isNftMarkerZero = startHint == std::numeric_limits<uint32_t>::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<Status, AccountCursor>
ngTraverseOwnedNodes(
BackendInterface const& backend,
ripple::AccountID const& accountID,
std::uint32_t sequence,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> 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<Status>(&cursorMaybe))
return *status;
auto const [nextNFTPage, nftsCount] = std::get<AccountCursor>(cursorMaybe);
// if limit reach , we return the next page and max as marker
if (nftsCount >= limit)
return AccountCursor{nextNFTPage, std::numeric_limits<uint32_t>::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<Status, AccountCursor>
@@ -470,7 +524,6 @@ traverseOwnedNodes(
std::uint32_t const startHint,
std::uint32_t sequence,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> 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};

View File

@@ -101,16 +101,6 @@ getLedgerInfoFromHashOrSeq(
std::optional<uint32_t> ledgerIndex,
uint32_t maxSeq);
std::variant<Status, AccountCursor>
traverseOwnedNodes(
BackendInterface const& backend,
ripple::AccountID const& accountID,
std::uint32_t sequence,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> atOwnedNode);
std::variant<Status, AccountCursor>
traverseOwnedNodes(
BackendInterface const& backend,
@@ -119,21 +109,21 @@ traverseOwnedNodes(
std::uint32_t const startHint,
std::uint32_t sequence,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> atOwnedNode);
// Remove the account check from traverseOwnedNodes
// Account check has been done by framework,remove it from internal function
std::variant<Status, AccountCursor>
ngTraverseOwnedNodes(
traverseOwnedNodes(
BackendInterface const& backend,
ripple::AccountID const& accountID,
std::uint32_t sequence,
std::uint32_t limit,
std::optional<std::string> jsonCursor,
boost::asio::yield_context& yield,
std::function<void(ripple::SLE&&)> atOwnedNode);
std::function<void(ripple::SLE&&)> atOwnedNode,
bool nftIncluded = false);
std::shared_ptr<ripple::SLE const>
read(

View File

@@ -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{};

View File

@@ -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<Status>(&next))

View File

@@ -63,7 +63,7 @@ AccountCurrenciesHandler::process(AccountCurrenciesHandler::Input input, Context
};
// traverse all owned nodes, limit->max, marker->empty
ngTraverseOwnedNodes(
traverseOwnedNodes(
*sharedPtrBackend_,
*accountID,
lgrInfo.seq,

View File

@@ -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<Status>(&next))

View File

@@ -21,7 +21,6 @@
namespace RPC {
// document does not mention nft_page, we still support it tho
std::unordered_map<std::string, ripple::LedgerEntryType> 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<Status>(&next))
return Error{*status};

View File

@@ -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<Status>(&next))

View File

@@ -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,

View File

@@ -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));

View File

@@ -85,7 +85,7 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input input, Context const&
auto limit = input.limit;
ngTraverseOwnedNodes(
traverseOwnedNodes(
*sharedPtrBackend_,
*accountID,
lgrInfo.seq,