#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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(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(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 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) { adjustOwnerCount( view, view.peek(keylet::account(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("mergePages: pages passed in out of order!"); if ((*p1)[~sfNextPageMin] != p2->key()) Throw("mergePages: next link broken!"); if ((*p2)[~sfPreviousPageMin] != p1->key()) Throw("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("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( "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); int cnt = 0; if (prev && mergePages(view, prev, curr)) cnt--; if (next && mergePages(view, curr, next)) cnt--; if (cnt != 0) { adjustOwnerCount( view, view.peek(keylet::account(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); } adjustOwnerCount( view, view.peek(keylet::account(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); int 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++; adjustOwnerCount( view, view.peek(keylet::account(owner)), -1 * cnt, beast::Journal{beast::Journal::getNullSink()}); return tesSUCCESS; } std::optional 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 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(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 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( "Offer " + to_string(offerIndexes[i]) + " cannot be deleted!"); } } if (maxDeletableOffers == deletedOffersCount) break; } } while ((pageIndex.value_or(0) != 0u) && maxDeletableOffers != deletedOffersCount); 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, 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; adjustOwnerCount( view, view.peek(keylet::account(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(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( "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 const& dest, std::optional const& expiration, std::uint16_t nftFlags, Rules const& rules, std::optional 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 const& dest, std::uint16_t nftFlags, std::uint16_t xferFee, beast::Journal j, std::optional 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()))) return tecNO_LINE; } else if (!view.exists(keylet::trustLine(nftIssuer, amount.get()))) { return tecNO_LINE; } if (isFrozen(view, nftIssuer, amount.get().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().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()); if (!isTesSuccess(res)) return res; } return tesSUCCESS; } TER tokenOfferCreateApply( ApplyView& view, AccountID const& acctID, STAmount const& amount, std::optional const& dest, std::optional 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 < view.fees().accountReserve((*acct)[sfOwnerCount] + 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(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. adjustOwnerCount(view, view.peek(acctKeylet), 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