Skip to content

Commit

Permalink
Allow NFT to be burned when number of offers is greater than 500 (XRP…
Browse files Browse the repository at this point in the history
…LF#4346)

* Allow offers to be removable
* Delete sell offers first

Signed-off-by: Shawn Xie <shawnxie920@gmail.com>
  • Loading branch information
shawnxie999 authored and dangell7 committed Mar 5, 2023
1 parent 2fff4c5 commit 22d3d48
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 99 deletions.
46 changes: 40 additions & 6 deletions src/ripple/app/tx/impl/NFTokenBurn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx)
}
}

// If there are too many offers, then burning the token would produce too
// much metadata. Disallow burning a token with too many offers.
return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]);
if (!ctx.view.rules().enabled(fixUnburnableNFToken))
{
// If there are too many offers, then burning the token would produce
// too much metadata. Disallow burning a token with too many offers.
return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]);
}

return tesSUCCESS;
}

TER
Expand All @@ -104,9 +109,38 @@ NFTokenBurn::doApply()
view().update(issuer);
}

// Optimized deletion of all offers.
nft::removeAllTokenOffers(view(), keylet::nft_sells(ctx_.tx[sfNFTokenID]));
nft::removeAllTokenOffers(view(), keylet::nft_buys(ctx_.tx[sfNFTokenID]));
if (ctx_.view().rules().enabled(fixUnburnableNFToken))
{
// Delete up to 500 offers in total.
// Because the number of sell offers is likely to be less than
// the number of buy offers, we prioritize the deletion of sell
// offers in order to clean up sell offer directory
std::size_t const deletedSellOffers = nft::removeTokenOffersWithLimit(
view(),
keylet::nft_sells(ctx_.tx[sfNFTokenID]),
maxDeletableTokenOfferEntries);

if (maxDeletableTokenOfferEntries > deletedSellOffers)
{
nft::removeTokenOffersWithLimit(
view(),
keylet::nft_buys(ctx_.tx[sfNFTokenID]),
maxDeletableTokenOfferEntries - deletedSellOffers);
}
}
else
{
// Deletion of all offers.
nft::removeTokenOffersWithLimit(
view(),
keylet::nft_sells(ctx_.tx[sfNFTokenID]),
std::numeric_limits<int>::max());

nft::removeTokenOffersWithLimit(
view(),
keylet::nft_buys(ctx_.tx[sfNFTokenID]),
std::numeric_limits<int>::max());
}

return tesSUCCESS;
}
Expand Down
67 changes: 44 additions & 23 deletions src/ripple/app/tx/impl/details/NFTokenUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,34 +520,55 @@ findTokenAndPage(
}
return std::nullopt;
}
void
removeAllTokenOffers(ApplyView& view, Keylet const& directory)
{
view.dirDelete(directory, [&view](uint256 const& id) {
auto offer = view.peek(Keylet{ltNFTOKEN_OFFER, id});

if (!offer)
Throw<std::runtime_error>(
"Offer " + to_string(id) + " not found in ledger!");
std::size_t
removeTokenOffersWithLimit(
ApplyView& view,
Keylet const& directory,
std::size_t maxDeletableOffers)
{
if (maxDeletableOffers == 0)
return 0;

auto const owner = (*offer)[sfOwner];
std::optional<std::uint64_t> pageIndex{0};
std::size_t deletedOffersCount = 0;

if (!view.dirRemove(
keylet::ownerDir(owner),
(*offer)[sfOwnerNode],
offer->key(),
false))
Throw<std::runtime_error>(
"Offer " + to_string(id) + " not found in owner directory!");
do
{
auto const page = view.peek(keylet::page(directory, *pageIndex));
if (!page)
break;

// We get the index of the next page in case the current
// page is deleted after all of its entries have been removed
pageIndex = (*page)[~sfIndexNext];

auto offerIndexes = page->getFieldV256(sfIndexes);

// We reverse-iterate the offer directory page to delete all entries.
// Deleting an entry in a NFTokenOffer directory page won't cause
// entries from other pages to move to the current, so, it is safe to
// delete entries one by one in the page. It is required to iterate
// backwards to handle iterator invalidation for vector, as we are
// deleting during iteration.
for (int i = offerIndexes.size() - 1; i >= 0; --i)
{
if (auto const offer = view.peek(keylet::nftoffer(offerIndexes[i])))
{
if (deleteTokenOffer(view, offer))
++deletedOffersCount;
else
Throw<std::runtime_error>(
"Offer " + to_string(offerIndexes[i]) +
" cannot be deleted!");
}

adjustOwnerCount(
view,
view.peek(keylet::account(owner)),
-1,
beast::Journal{beast::Journal::getNullSink()});
if (maxDeletableOffers == deletedOffersCount)
break;
}
} while (pageIndex.value_or(0) && maxDeletableOffers != deletedOffersCount);

view.erase(offer);
});
return deletedOffersCount;
}

TER
Expand Down
10 changes: 7 additions & 3 deletions src/ripple/app/tx/impl/details/NFTokenUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ constexpr std::uint16_t const flagOnlyXRP = 0x0002;
constexpr std::uint16_t const flagCreateTrustLines = 0x0004;
constexpr std::uint16_t const flagTransferable = 0x0008;

/** Deletes all offers from the specified token offer directory. */
void
removeAllTokenOffers(ApplyView& view, Keylet const& directory);
/** Delete up to a specified number of offers from the specified token offer
* directory. */
std::size_t
removeTokenOffersWithLimit(
ApplyView& view,
Keylet const& directory,
std::size_t maxDeletableOffers);

/** Returns tesSUCCESS if NFToken has few enough offers that it can be burned */
TER
Expand Down
Loading

0 comments on commit 22d3d48

Please sign in to comment.