Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow NFT to be burned when number of offers is greater than 500 (part of NonFungibleTokensV1_2 amendment) #4346

Merged
merged 18 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
intelliot marked this conversation as resolved.
Show resolved Hide resolved
{
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());
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}

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.
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
for (int i = offerIndexes.size() - 1; i >= 0; --i)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
{
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