Files
rippled/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
2026-07-01 18:17:29 -04:00

1078 lines
34 KiB
C++

#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/nft.h>
#include <xrpl/protocol/nftPageMask.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <iterator>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
namespace xrpl::nft {
static SLE::const_pointer
locatePage(ReadView const& view, AccountID const& owner, uint256 const& id)
{
auto const first = keylet::nftokenPage(keylet::nftokenPageMin(owner), id);
auto const last = keylet::nftokenPageMax(owner);
// This NFT can only be found in the first page with a key that's strictly
// greater than `first`, so look for that, up until the maximum possible
// page.
return view.read(
Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
}
static SLE::pointer
locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
{
auto const first = keylet::nftokenPage(keylet::nftokenPageMin(owner), id);
auto const last = keylet::nftokenPageMax(owner);
// This NFT can only be found in the first page with a key that's strictly
// greater than `first`, so look for that, up until the maximum possible
// page.
return view.peek(
Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
}
static SLE::pointer
getPageForToken(
ApplyView& view,
AccountID const& owner,
uint256 const& id,
std::function<void(ApplyView&, AccountID const&)> const& createCallback)
{
auto const base = keylet::nftokenPageMin(owner);
auto const first = keylet::nftokenPage(base, id);
auto const last = keylet::nftokenPageMax(owner);
// This NFT can only be found in the first page with a key that's strictly
// greater than `first`, so look for that, up until the maximum possible
// page.
auto cp =
view.peek(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
// A suitable page doesn't exist; we'll have to create one.
if (!cp)
{
STArray const arr;
cp = std::make_shared<SLE>(last);
cp->setFieldArray(sfNFTokens, arr);
view.insert(cp);
createCallback(view, owner);
return cp;
}
STArray narr = cp->getFieldArray(sfNFTokens);
// The right page still has space: we're good.
if (narr.size() != kDirMaxTokensPerPage)
return cp;
// We need to split the page in two: the first half of the items in this
// page will go into the new page; the rest will stay with the existing
// page.
//
// Note we can't always split the page exactly in half. All equivalent
// NFTs must be kept on the same page. So when the page contains
// equivalent NFTs, the split may be lopsided in order to keep equivalent
// NFTs on the same page.
STArray carr;
{
// We prefer to keep equivalent NFTs on a page boundary. That gives
// any additional equivalent NFTs maximum room for expansion.
// Round up the boundary until there's a non-equivalent entry.
uint256 const cmp =
narr[(kDirMaxTokensPerPage / 2) - 1].getFieldH256(sfNFTokenID) & nft::kPageMask;
// Note that the calls to find_if_not() and (later) find_if()
// rely on the fact that narr is kept in sorted order.
auto splitIter = std::find_if_not(
narr.begin() + (kDirMaxTokensPerPage / 2), narr.end(), [&cmp](STObject const& obj) {
return (obj.getFieldH256(sfNFTokenID) & nft::kPageMask) == cmp;
});
// If we get all the way from the middle to the end with only
// equivalent NFTokens then check the front of the page for a
// place to make the split.
if (splitIter == narr.end())
{
splitIter = std::ranges::find_if(narr, [&cmp](STObject const& obj) {
return (obj.getFieldH256(sfNFTokenID) & nft::kPageMask) == cmp;
});
}
// There should be no circumstance when splitIter == end(), but if it
// were to happen we should bail out because something is confused.
if (splitIter == narr.end())
return nullptr;
// If splitIter == begin(), then the entire page is filled with
// equivalent tokens. This requires special handling.
if (splitIter == narr.begin())
{
auto const relation{(id & nft::kPageMask) <=> cmp};
if (relation == 0)
{
// If the passed in id belongs exactly on this (full) page
// this account simply cannot store the NFT.
return nullptr;
}
if (relation > 0)
{
// We need to leave the entire contents of this page in
// narr so carr stays empty. The new NFT will be
// inserted in carr. This keeps the NFTs that must be
// together all on their own page.
splitIter = narr.end();
}
// If neither of those conditions apply then put all of
// narr into carr and produce an empty narr where the new NFT
// will be inserted. Leave the split at narr.begin().
}
// Split narr at splitIter.
STArray newCarr(std::make_move_iterator(splitIter), std::make_move_iterator(narr.end()));
narr.erase(splitIter, narr.end());
std::swap(carr, newCarr);
}
// Determine the ID for the page index.
//
// Note that we use uint256::next() because there's a subtlety in the way
// NFT pages are structured. The low 96-bits of NFT ID must be strictly
// less than the low 96-bits of the enclosing page's index. In order to
// accommodate that requirement we use an index one higher than the
// largest NFT in the page.
uint256 const tokenIDForNewPage = narr.size() == kDirMaxTokensPerPage
? narr[kDirMaxTokensPerPage - 1].getFieldH256(sfNFTokenID).next()
: carr[0].getFieldH256(sfNFTokenID);
auto np = std::make_shared<SLE>(keylet::nftokenPage(base, tokenIDForNewPage));
XRPL_ASSERT(np->key() > base.key, "xrpl::nft::getPageForToken : valid NFT page index");
np->setFieldArray(sfNFTokens, narr);
np->setFieldH256(sfNextPageMin, cp->key());
if (auto ppm = (*cp)[~sfPreviousPageMin])
{
np->setFieldH256(sfPreviousPageMin, *ppm);
if (auto p3 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm)))
{
p3->setFieldH256(sfNextPageMin, np->key());
view.update(p3);
}
}
view.insert(np);
cp->setFieldArray(sfNFTokens, carr);
cp->setFieldH256(sfPreviousPageMin, np->key());
view.update(cp);
createCallback(view, owner);
return (first.key < np->key()) ? np : cp;
}
bool
compareTokens(uint256 const& a, uint256 const& b)
{
// The sort of NFTokens needs to be fully deterministic, but the sort
// is weird because we sort on the low 96-bits first. But if the low
// 96-bits are identical we still need a fully deterministic sort.
// So we sort on the low 96-bits first. If those are equal we sort on
// the whole thing.
if (auto const lowBitsCmp{(a & nft::kPageMask) <=> (b & nft::kPageMask)}; lowBitsCmp != 0)
return lowBitsCmp < 0;
return a < b;
}
TER
changeTokenURI(
ApplyView& view,
AccountID const& owner,
uint256 const& nftokenID,
std::optional<xrpl::Slice> const& uri)
{
SLE::pointer const page = locatePage(view, owner, nftokenID);
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return tecINTERNAL; // LCOV_EXCL_LINE
// Locate the NFT in the page
STArray& arr = page->peekFieldArray(sfNFTokens);
auto const nftIter = std::ranges::find_if(
arr, [&nftokenID](STObject const& obj) { return (obj[sfNFTokenID] == nftokenID); });
if (nftIter == arr.end())
return tecINTERNAL; // LCOV_EXCL_LINE
if (uri)
{
nftIter->setFieldVL(sfURI, *uri);
}
else if (nftIter->isFieldPresent(sfURI))
{
nftIter->makeFieldAbsent(sfURI);
}
view.update(page);
return tesSUCCESS;
}
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, AccountID owner, STObject&& nft)
{
XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token");
// First, we need to locate the page the NFT belongs to, creating it
// if necessary. This operation may fail if it is impossible to insert
// the NFT.
SLE::pointer const page =
getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
increaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
});
if (!page)
return tecNO_SUITABLE_NFTOKEN_PAGE;
{
auto arr = page->getFieldArray(sfNFTokens);
arr.pushBack(std::move(nft));
arr.sort([](STObject const& o1, STObject const& o2) {
return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID));
});
page->setFieldArray(sfNFTokens, arr);
}
view.update(page);
return tesSUCCESS;
}
static bool
mergePages(ApplyView& view, SLE::ref p1, SLE::ref p2)
{
if (p1->key() >= p2->key())
Throw<std::runtime_error>("mergePages: pages passed in out of order!");
if ((*p1)[~sfNextPageMin] != p2->key())
Throw<std::runtime_error>("mergePages: next link broken!");
if ((*p2)[~sfPreviousPageMin] != p1->key())
Throw<std::runtime_error>("mergePages: previous link broken!");
auto const p1arr = p1->getFieldArray(sfNFTokens);
auto const p2arr = p2->getFieldArray(sfNFTokens);
// Now check whether to merge the two pages; it only makes sense to do
// this it would mean that one of them can be deleted as a result of
// the merge.
if (p1arr.size() + p2arr.size() > kDirMaxTokensPerPage)
return false;
STArray x(p1arr.size() + p2arr.size());
std::ranges::merge(
p1arr, p2arr, std::back_inserter(x), [](STObject const& a, STObject const& b) {
return compareTokens(a.getFieldH256(sfNFTokenID), b.getFieldH256(sfNFTokenID));
});
p2->setFieldArray(sfNFTokens, x);
// So, at this point we need to unlink "p1" (since we just emptied it) but
// we need to first relink the directory: if p1 has a previous page (p0),
// load it, point it to p2 and point p2 to it.
p2->makeFieldAbsent(sfPreviousPageMin);
if (auto const ppm = (*p1)[~sfPreviousPageMin])
{
auto p0 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm));
if (!p0)
Throw<std::runtime_error>("mergePages: p0 can't be located!");
p0->setFieldH256(sfNextPageMin, p2->key());
view.update(p0);
p2->setFieldH256(sfPreviousPageMin, *ppm);
}
view.update(p2);
view.erase(p1);
return true;
}
/** Remove the token from the owner's token directory. */
TER
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
{
SLE::pointer const page = locatePage(view, owner, nftokenID);
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return tecNO_ENTRY;
return removeToken(view, owner, nftokenID, page);
}
/** Remove the token from the owner's token directory. */
TER
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, SLE::ref curr)
{
// We found a page, but the given NFT may not be in it.
auto arr = curr->getFieldArray(sfNFTokens);
{
auto x = std::ranges::find_if(
arr, [&nftokenID](STObject const& obj) { return (obj[sfNFTokenID] == nftokenID); });
if (x == arr.end())
return tecNO_ENTRY;
arr.erase(x);
}
// Page management:
auto const loadPage = [&view](SLE::ref page1, SF_UINT256 const& field) {
SLE::pointer page2;
if (auto const id = (*page1)[~field])
{
page2 = view.peek(Keylet(ltNFTOKEN_PAGE, *id));
if (!page2)
{
Throw<std::runtime_error>(
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
" field pointing to " + to_string(*id));
}
}
return page2;
};
auto const prev = loadPage(curr, sfPreviousPageMin);
auto const next = loadPage(curr, sfNextPageMin);
if (!arr.empty())
{
// The current page isn't empty. Update it and then try to consolidate
// pages. Note that this consolidation attempt may actually merge three
// pages into one!
curr->setFieldArray(sfNFTokens, arr);
view.update(curr);
std::uint32_t cnt = 0;
if (prev && mergePages(view, prev, curr))
++cnt;
if (next && mergePages(view, curr, next))
++cnt;
if (cnt != 0)
{
decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()});
}
return tesSUCCESS;
}
if (prev)
{
// With fixNFTokenPageLinks...
// The page is empty and there is a prev. If the last page of the
// directory is empty then we need to:
// 1. Move the contents of the previous page into the last page.
// 2. Fix up the link from prev's previous page.
// 3. Fix up the owner count.
// 4. Erase the previous page.
if (view.rules().enabled(fixNFTokenPageLinks) &&
((curr->key() & nft::kPageMask) == kPageMask))
{
// Copy all relevant information from prev to curr.
curr->peekFieldArray(sfNFTokens) = prev->peekFieldArray(sfNFTokens);
if (auto const prevLink = prev->at(~sfPreviousPageMin))
{
curr->at(sfPreviousPageMin) = *prevLink;
// Also fix up the NextPageMin link in the new Previous.
auto const newPrev = loadPage(curr, sfPreviousPageMin);
newPrev->at(sfNextPageMin) = curr->key();
view.update(newPrev);
}
else
{
curr->makeFieldAbsent(sfPreviousPageMin);
}
decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
view.update(curr);
view.erase(prev);
return tesSUCCESS;
}
// The page is empty and not the last page, so we can just unlink it
// and then remove it.
if (next)
{
prev->setFieldH256(sfNextPageMin, next->key());
}
else
{
prev->makeFieldAbsent(sfNextPageMin);
}
view.update(prev);
}
if (next)
{
// Make our next page point to our previous page:
if (prev)
{
next->setFieldH256(sfPreviousPageMin, prev->key());
}
else
{
next->makeFieldAbsent(sfPreviousPageMin);
}
view.update(next);
}
view.erase(curr);
uint32_t cnt = 1;
// Since we're here, try to consolidate the previous and current pages
// of the page we removed (if any) into one. mergePages() _should_
// always return false. Since tokens are burned one at a time, there
// should never be a page containing one token sitting between two pages
// that have few enough tokens that they can be merged.
//
// But, in case that analysis is wrong, it's good to leave this code here
// just in case.
if (prev && next &&
mergePages(
view,
view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())),
view.peek(Keylet(ltNFTOKEN_PAGE, next->key()))))
cnt++;
decreaseOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()});
return tesSUCCESS;
}
std::optional<STObject>
findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID)
{
SLE::const_pointer const page = locatePage(view, owner, nftokenID);
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return std::nullopt;
// We found a candidate page, but the given NFT may not be in it.
for (auto const& t : page->getFieldArray(sfNFTokens))
{
if (t[sfNFTokenID] == nftokenID)
return t;
}
return std::nullopt;
}
std::optional<TokenAndPage>
findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
{
SLE::pointer page = locatePage(view, owner, nftokenID);
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return std::nullopt;
// We found a candidate page, but the given NFT may not be in it.
for (auto const& t : page->getFieldArray(sfNFTokens))
{
if (t[sfNFTokenID] == nftokenID)
{
// This std::optional constructor is explicit, so it is spelled out.
return std::optional<TokenAndPage>(std::in_place, t, std::move(page));
}
}
return std::nullopt;
}
std::size_t
removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t maxDeletableOffers)
{
if (maxDeletableOffers == 0)
return 0;
std::optional<std::uint64_t> pageIndex{0};
std::size_t deletedOffersCount = 0;
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::nftokenOffer(offerIndexes[i])))
{
if (deleteTokenOffer(view, offer))
{
++deletedOffersCount;
}
else
{
Throw<std::runtime_error>(
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
}
}
if (maxDeletableOffers == deletedOffersCount)
break;
}
} while ((pageIndex.value_or(0) != 0u) && maxDeletableOffers != deletedOffersCount);
return deletedOffersCount;
}
bool
deleteTokenOffer(ApplyView& view, SLE::ref offer)
{
if (offer->getType() != ltNFTOKEN_OFFER)
return false;
auto const owner = (*offer)[sfOwner];
if (!view.dirRemove(keylet::ownerDir(owner), (*offer)[sfOwnerNode], offer->key(), false))
return false;
auto const nftokenID = (*offer)[sfNFTokenID];
if (!view.dirRemove(
offer->isFlag(lsfSellNFToken) ? keylet::nftSells(nftokenID)
: keylet::nftBuys(nftokenID),
(*offer)[sfNFTokenOfferNode],
offer->key(),
false))
return false;
decreaseOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
view.erase(offer);
return true;
}
bool
repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
{
bool didRepair = false;
auto const last = keylet::nftokenPageMax(owner);
SLE::pointer page = view.peek(Keylet(
ltNFTOKEN_PAGE,
view.succ(keylet::nftokenPageMin(owner).key, last.key.next()).value_or(last.key)));
if (!page)
return didRepair;
if (page->key() == last.key)
{
// There's only one page in this entire directory. There should be
// no links on that page.
bool const nextPresent = page->isFieldPresent(sfNextPageMin);
bool const prevPresent = page->isFieldPresent(sfPreviousPageMin);
if (nextPresent || prevPresent)
{
didRepair = true;
if (prevPresent)
page->makeFieldAbsent(sfPreviousPageMin);
if (nextPresent)
page->makeFieldAbsent(sfNextPageMin);
view.update(page);
}
return didRepair;
}
// First page is not the same as last page. The first page should not
// contain a previous link.
if (page->isFieldPresent(sfPreviousPageMin))
{
didRepair = true;
page->makeFieldAbsent(sfPreviousPageMin);
view.update(page);
}
SLE::pointer nextPage;
while (
(nextPage = view.peek(Keylet(
ltNFTOKEN_PAGE, view.succ(page->key().next(), last.key.next()).value_or(last.key)))))
{
if (!page->isFieldPresent(sfNextPageMin) ||
page->getFieldH256(sfNextPageMin) != nextPage->key())
{
didRepair = true;
page->setFieldH256(sfNextPageMin, nextPage->key());
view.update(page);
}
if (!nextPage->isFieldPresent(sfPreviousPageMin) ||
nextPage->getFieldH256(sfPreviousPageMin) != page->key())
{
didRepair = true;
nextPage->setFieldH256(sfPreviousPageMin, page->key());
view.update(nextPage);
}
if (nextPage->key() == last.key)
{
// We need special handling for the last page.
break;
}
page = nextPage;
}
// When we arrive here, nextPage should have the same index as last.
// If not, then that's something we need to fix.
if (!nextPage)
{
// It turns out that page is the last page for this owner, but
// that last page does not have the expected final index. We need
// to move the contents of the current last page into a page with the
// correct index.
//
// The owner count does not need to change because, even though
// we're adding a page, we'll also remove the page that used to be
// last.
didRepair = true;
nextPage = std::make_shared<SLE>(last);
// Copy all relevant information from prev to curr.
nextPage->peekFieldArray(sfNFTokens) = page->peekFieldArray(sfNFTokens);
if (auto const prevLink = page->at(~sfPreviousPageMin))
{
nextPage->at(sfPreviousPageMin) = *prevLink;
// Also fix up the NextPageMin link in the new Previous.
auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink));
if (!newPrev)
{
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. Unexpected link problem.");
}
newPrev->at(sfNextPageMin) = nextPage->key();
view.update(newPrev);
}
view.erase(page);
view.insert(nextPage);
return didRepair;
}
XRPL_ASSERT(nextPage, "xrpl::nft::repairNFTokenDirectoryLinks : next page is available");
if (nextPage->isFieldPresent(sfNextPageMin))
{
didRepair = true;
nextPage->makeFieldAbsent(sfNextPageMin);
view.update(nextPage);
}
return didRepair;
}
NotTEC
tokenOfferCreatePreflight(
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,
std::optional<std::uint32_t> const& expiration,
std::uint16_t nftFlags,
Rules const& rules,
std::optional<AccountID> const& owner,
std::uint32_t txFlags)
{
if (amount.negative())
{
// An offer for a negative amount makes no sense.
return temBAD_AMOUNT;
}
if (!isXRP(amount))
{
if ((nftFlags & nft::kFlagOnlyXrp) != 0)
return temBAD_AMOUNT;
if (!amount)
return temBAD_AMOUNT;
}
// If this is an offer to buy, you must offer something; if it's an
// offer to sell, you can ask for nothing.
bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
if (!isSellOffer && !amount)
return temBAD_AMOUNT;
if (expiration.has_value() && expiration.value() == 0)
return temBAD_EXPIRATION;
// The 'Owner' field must be present when offering to buy, but can't
// be present when selling (it's implicit):
if (owner.has_value() == isSellOffer)
return temMALFORMED;
if (owner && owner == acctID)
return temMALFORMED;
// The destination can't be the account executing the transaction.
if (dest && dest == acctID)
{
return temMALFORMED;
}
return tesSUCCESS;
}
TER
tokenOfferCreatePreclaim(
ReadView const& view,
AccountID const& acctID,
AccountID const& nftIssuer,
STAmount const& amount,
std::optional<AccountID> const& dest,
std::uint16_t nftFlags,
std::uint16_t xferFee,
beast::Journal j,
std::optional<AccountID> const& owner,
std::uint32_t txFlags)
{
if (((nftFlags & nft::kFlagCreateTrustLines) == 0) && !amount.native() && (xferFee != 0u))
{
if (!view.exists(keylet::account(nftIssuer)))
return tecNO_ISSUER;
// If the IOU issuer and the NFToken issuer are the same, then that
// issuer does not need a trust line to accept their fee.
if (view.rules().enabled(featureNFTokenMintOffer))
{
if (nftIssuer != amount.getIssuer() &&
!view.read(keylet::trustLine(nftIssuer, amount.get<Issue>())))
return tecNO_LINE;
}
else if (!view.exists(keylet::trustLine(nftIssuer, amount.get<Issue>())))
{
return tecNO_LINE;
}
if (isFrozen(view, nftIssuer, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
}
if (nftIssuer != acctID && ((nftFlags & nft::kFlagTransferable) == 0))
{
auto const root = view.read(keylet::account(nftIssuer));
XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
return tefNFTOKEN_IS_NOT_TRANSFERABLE;
}
if (isFrozen(view, acctID, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
// If this is an offer to buy the token, the account must have the
// needed funds at hand; but note that funds aren't reserved and the
// offer may later become unfunded.
if ((txFlags & tfSellNFToken) == 0)
{
// We allow an IOU issuer to make a buy offer
// using their own currency.
if (accountFunds(view, acctID, amount, FreezeHandling::ZeroIfFrozen, j).signum() <= 0)
return tecUNFUNDED_OFFER;
}
if (dest)
{
// If a destination is specified, the destination must already be in
// the ledger.
auto const sleDst = view.read(keylet::account(*dest));
if (!sleDst)
return tecNO_DST;
// check if the destination has disallowed incoming offers
if (sleDst->isFlag(lsfDisallowIncomingNFTokenOffer))
return tecNO_PERMISSION;
}
if (owner)
{
auto const sleOwner = view.read(keylet::account(*owner));
// defensively check
// it should not be possible to specify owner that doesn't exist
if (!sleOwner)
return tecNO_TARGET;
if (sleOwner->isFlag(lsfDisallowIncomingNFTokenOffer))
return tecNO_PERMISSION;
}
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native())
{
// If this is a sell offer, check that the account is allowed to
// receive IOUs. If this is a buy offer, we have to check that trustline
// is authorized, even though we previously checked it's balance via
// accountHolds. This is due to a possibility of existence of
// unauthorized trustlines with balance
auto const res =
nft::checkTrustlineAuthorized(view, acctID, j, amount.asset().get<Issue>());
if (!isTesSuccess(res))
return res;
}
return tesSUCCESS;
}
TER
tokenOfferCreateApply(
ApplyView& view,
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,
std::optional<std::uint32_t> const& expiration,
SeqProxy seqProxy,
uint256 const& nftokenID,
XRPAmount const& priorBalance,
beast::Journal j,
std::uint32_t txFlags)
{
Keylet const acctKeylet = keylet::account(acctID);
if (auto const acct = view.read(acctKeylet);
priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1}))
return tecINSUFFICIENT_RESERVE;
auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value());
// Create the offer:
{
// Token offers are always added to the owner's owner directory:
auto const ownerNode =
view.dirInsert(keylet::ownerDir(acctID), offerID, describeOwnerDir(acctID));
if (!ownerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
// Token offers are also added to the token's buy or sell offer
// directory
auto const offerNode = view.dirInsert(
isSellOffer ? keylet::nftSells(nftokenID) : keylet::nftBuys(nftokenID),
offerID,
[&nftokenID, isSellOffer](SLE::ref sle) {
(*sle)[sfFlags] = isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
(*sle)[sfNFTokenID] = nftokenID;
});
if (!offerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
std::uint32_t sleFlags = 0;
if (isSellOffer)
sleFlags |= lsfSellNFToken;
auto offer = std::make_shared<SLE>(offerID);
(*offer)[sfOwner] = acctID;
(*offer)[sfNFTokenID] = nftokenID;
(*offer)[sfAmount] = amount;
(*offer)[sfFlags] = sleFlags;
(*offer)[sfOwnerNode] = *ownerNode;
(*offer)[sfNFTokenOfferNode] = *offerNode;
if (expiration)
(*offer)[sfExpiration] = *expiration;
if (dest)
(*offer)[sfDestination] = *dest;
view.insert(offer);
}
// Update owner count.
increaseOwnerCount(view, acctID, {}, 1, j);
return tesSUCCESS;
}
TER
checkTrustlineAuthorized(
ReadView const& view,
AccountID const id,
beast::Journal const j,
Issue const& issue)
{
// Only valid for custom currencies
XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineAuthorized : valid to check.");
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
{
auto const issuerAccount = view.read(keylet::account(issue.account));
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
"receive IOUs from non-existent issuer: "
<< to_string(issue.account);
return tecNO_ISSUER;
}
// An account can not create a trustline to itself, so no line can
// exist to be authorized. Additionally, an issuer can always accept
// its own issuance.
if (issue.account == id)
{
return tesSUCCESS;
}
if (issuerAccount->isFlag(lsfRequireAuth))
{
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
if (!trustLine)
{
return tecNO_LINE;
}
// Entries have a canonical representation, determined by a
// lexicographical "greater than" comparison employing strict
// weak ordering. Determine which entry we need to access.
if (!trustLine->isFlag(id > issue.account ? lsfLowAuth : lsfHighAuth))
{
return tecNO_AUTH;
}
}
}
return tesSUCCESS;
}
TER
checkTrustlineDeepFrozen(
ReadView const& view,
AccountID const id,
beast::Journal const j,
Issue const& issue)
{
// Only valid for custom currencies
XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineDeepFrozen : valid to check.");
if (view.rules().enabled(featureDeepFreeze))
{
auto const issuerAccount = view.read(keylet::account(issue.account));
if (!issuerAccount)
{
JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
"receive IOUs from non-existent issuer: "
<< to_string(issue.account);
return tecNO_ISSUER;
}
// An account can not create a trustline to itself, so no line can
// exist to be frozen. Additionally, an issuer can always accept its
// own issuance.
if (issue.account == id)
{
return tesSUCCESS;
}
auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
if (!trustLine)
{
return tesSUCCESS;
}
// There's no difference which side enacted deep freeze, accepting
// tokens shouldn't be possible.
bool const deepFrozen =
((*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze)) != 0u;
if (deepFrozen)
{
return tecFROZEN;
}
}
return tesSUCCESS;
}
} // namespace xrpl::nft