diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 3af81eff16..d8dac4caaf 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -20,6 +20,10 @@ removeTokenOffersWithLimit( Keylet const& directory, std::size_t maxDeletableOffers); +/** Returns tesSUCCESS if NFToken has few enough offers that it can be burned */ +TER +notTooManyOffers(ReadView const& view, uint256 const& nftokenID); + /** Finds the specified token in the owner's token directory. */ std::optional findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID); diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 7e7335232f..4652bccca8 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -607,6 +607,33 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t return deletedOffersCount; } +TER +notTooManyOffers(ReadView const& view, uint256 const& nftokenID) +{ + std::size_t totalOffers = 0; + + { + Dir const buys(view, keylet::nft_buys(nftokenID)); + for (auto iter = buys.begin(); iter != buys.end(); iter.next_page()) + { + totalOffers += iter.page_size(); + if (totalOffers > maxDeletableTokenOfferEntries) + return tefTOO_BIG; + } + } + + { + Dir const sells(view, keylet::nft_sells(nftokenID)); + for (auto iter = sells.begin(); iter != sells.end(); iter.next_page()) + { + totalOffers += iter.page_size(); + if (totalOffers > maxDeletableTokenOfferEntries) + return tefTOO_BIG; + } + } + return tesSUCCESS; +} + bool deleteTokenOffer(ApplyView& view, std::shared_ptr const& offer) { diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp index a4623e7acc..2631c55adb 100644 --- a/src/xrpld/app/misc/detail/ValidatorSite.cpp +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp @@ -129,7 +129,11 @@ ValidatorSite::load( { try { - sites_.emplace_back(uri); + // This is not super efficient, but it doesn't happen often. + bool found = std::ranges::any_of( + sites_, [&uri](auto const& site) { return site.loadedResource->uri == uri; }); + if (!found) + sites_.emplace_back(uri); } catch (std::exception const& e) { @@ -190,6 +194,16 @@ ValidatorSite::setTimer( std::lock_guard const& site_lock, std::lock_guard const& state_lock) { + if (!sites_.empty() && // + std::ranges::all_of( + sites_, [](auto const& site) { return site.lastRefreshStatus.has_value(); })) + { + // If all of the sites have been handled at least once (including + // errors and timeouts), call missingSite, which will load the cache + // files for any lists that are still unavailable. + missingSite(site_lock); + } + auto next = std::min_element(sites_.begin(), sites_.end(), [](Site const& a, Site const& b) { return a.nextRefresh < b.nextRefresh; }); @@ -300,7 +314,7 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec) // processes a network error. Usually, this function runs first, // but on extremely rare occasions, the response handler can run // first, which will leave activeResource empty. - auto const& site = sites_[siteIdx]; + auto& site = sites_[siteIdx]; if (site.activeResource) { JLOG(j_.warn()) << "Request for " << site.activeResource->uri << " took too long"; @@ -308,6 +322,9 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec) else JLOG(j_.error()) << "Request took too long, but a response has " "already been processed"; + if (!site.lastRefreshStatus) + site.lastRefreshStatus.emplace( + Site::Status{clock_type::now(), ListDisposition::invalid, "timeout"}); } std::lock_guard const lock_state{state_mutex_};