mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-24 15:40:26 +00:00
1078 lines
34 KiB
C++
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
|