rippled
Loading...
Searching...
No Matches
NFTokenUtils.cpp
1#include <xrpld/app/tx/detail/NFTokenUtils.h>
2
3#include <xrpl/basics/algorithm.h>
4#include <xrpl/ledger/Dir.h>
5#include <xrpl/ledger/View.h>
6#include <xrpl/protocol/Feature.h>
7#include <xrpl/protocol/STArray.h>
8#include <xrpl/protocol/TxFlags.h>
9#include <xrpl/protocol/nftPageMask.h>
10
11#include <functional>
12#include <memory>
13
14namespace xrpl {
15
16namespace nft {
17
19locatePage(ReadView const& view, AccountID const& owner, uint256 const& id)
20{
21 auto const first = keylet::nftpage(keylet::nftpage_min(owner), id);
22 auto const last = keylet::nftpage_max(owner);
23
24 // This NFT can only be found in the first page with a key that's strictly
25 // greater than `first`, so look for that, up until the maximum possible
26 // page.
27 return view.read(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
28}
29
31locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
32{
33 auto const first = keylet::nftpage(keylet::nftpage_min(owner), id);
34 auto const last = keylet::nftpage_max(owner);
35
36 // This NFT can only be found in the first page with a key that's strictly
37 // greater than `first`, so look for that, up until the maximum possible
38 // page.
39 return view.peek(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
40}
41
44 ApplyView& view,
45 AccountID const& owner,
46 uint256 const& id,
47 std::function<void(ApplyView&, AccountID const&)> const& createCallback)
48{
49 auto const base = keylet::nftpage_min(owner);
50 auto const first = keylet::nftpage(base, id);
51 auto const last = keylet::nftpage_max(owner);
52
53 // This NFT can only be found in the first page with a key that's strictly
54 // greater than `first`, so look for that, up until the maximum possible
55 // page.
56 auto cp = view.peek(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
57
58 // A suitable page doesn't exist; we'll have to create one.
59 if (!cp)
60 {
61 STArray arr;
62 cp = std::make_shared<SLE>(last);
63 cp->setFieldArray(sfNFTokens, arr);
64 view.insert(cp);
65 createCallback(view, owner);
66 return cp;
67 }
68
69 STArray narr = cp->getFieldArray(sfNFTokens);
70
71 // The right page still has space: we're good.
72 if (narr.size() != dirMaxTokensPerPage)
73 return cp;
74
75 // We need to split the page in two: the first half of the items in this
76 // page will go into the new page; the rest will stay with the existing
77 // page.
78 //
79 // Note we can't always split the page exactly in half. All equivalent
80 // NFTs must be kept on the same page. So when the page contains
81 // equivalent NFTs, the split may be lopsided in order to keep equivalent
82 // NFTs on the same page.
83 STArray carr;
84 {
85 // We prefer to keep equivalent NFTs on a page boundary. That gives
86 // any additional equivalent NFTs maximum room for expansion.
87 // Round up the boundary until there's a non-equivalent entry.
88 uint256 const cmp = narr[(dirMaxTokensPerPage / 2) - 1].getFieldH256(sfNFTokenID) & nft::pageMask;
89
90 // Note that the calls to find_if_not() and (later) find_if()
91 // rely on the fact that narr is kept in sorted order.
92 auto splitIter =
93 std::find_if_not(narr.begin() + (dirMaxTokensPerPage / 2), narr.end(), [&cmp](STObject const& obj) {
94 return (obj.getFieldH256(sfNFTokenID) & nft::pageMask) == cmp;
95 });
96
97 // If we get all the way from the middle to the end with only
98 // equivalent NFTokens then check the front of the page for a
99 // place to make the split.
100 if (splitIter == narr.end())
101 splitIter = std::find_if(narr.begin(), narr.end(), [&cmp](STObject const& obj) {
102 return (obj.getFieldH256(sfNFTokenID) & nft::pageMask) == cmp;
103 });
104
105 // There should be no circumstance when splitIter == end(), but if it
106 // were to happen we should bail out because something is confused.
107 if (splitIter == narr.end())
108 return nullptr;
109
110 // If splitIter == begin(), then the entire page is filled with
111 // equivalent tokens. This requires special handling.
112 if (splitIter == narr.begin())
113 {
114 auto const relation{(id & nft::pageMask) <=> cmp};
115 if (relation == 0)
116 {
117 // If the passed in id belongs exactly on this (full) page
118 // this account simply cannot store the NFT.
119 return nullptr;
120 }
121
122 if (relation > 0)
123 {
124 // We need to leave the entire contents of this page in
125 // narr so carr stays empty. The new NFT will be
126 // inserted in carr. This keeps the NFTs that must be
127 // together all on their own page.
128 splitIter = narr.end();
129 }
130
131 // If neither of those conditions apply then put all of
132 // narr into carr and produce an empty narr where the new NFT
133 // will be inserted. Leave the split at narr.begin().
134 }
135
136 // Split narr at splitIter.
137 STArray newCarr(std::make_move_iterator(splitIter), std::make_move_iterator(narr.end()));
138 narr.erase(splitIter, narr.end());
139 std::swap(carr, newCarr);
140 }
141
142 // Determine the ID for the page index.
143 //
144 // Note that we use uint256::next() because there's a subtlety in the way
145 // NFT pages are structured. The low 96-bits of NFT ID must be strictly
146 // less than the low 96-bits of the enclosing page's index. In order to
147 // accommodate that requirement we use an index one higher than the
148 // largest NFT in the page.
149 uint256 const tokenIDForNewPage = narr.size() == dirMaxTokensPerPage
150 ? narr[dirMaxTokensPerPage - 1].getFieldH256(sfNFTokenID).next()
151 : carr[0].getFieldH256(sfNFTokenID);
152
153 auto np = std::make_shared<SLE>(keylet::nftpage(base, tokenIDForNewPage));
154 XRPL_ASSERT(np->key() > base.key, "xrpl::nft::getPageForToken : valid NFT page index");
155 np->setFieldArray(sfNFTokens, narr);
156 np->setFieldH256(sfNextPageMin, cp->key());
157
158 if (auto ppm = (*cp)[~sfPreviousPageMin])
159 {
160 np->setFieldH256(sfPreviousPageMin, *ppm);
161
162 if (auto p3 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm)))
163 {
164 p3->setFieldH256(sfNextPageMin, np->key());
165 view.update(p3);
166 }
167 }
168
169 view.insert(np);
170
171 cp->setFieldArray(sfNFTokens, carr);
172 cp->setFieldH256(sfPreviousPageMin, np->key());
173 view.update(cp);
174
175 createCallback(view, owner);
176
177 return (first.key < np->key()) ? np : cp;
178}
179
180bool
181compareTokens(uint256 const& a, uint256 const& b)
182{
183 // The sort of NFTokens needs to be fully deterministic, but the sort
184 // is weird because we sort on the low 96-bits first. But if the low
185 // 96-bits are identical we still need a fully deterministic sort.
186 // So we sort on the low 96-bits first. If those are equal we sort on
187 // the whole thing.
188 if (auto const lowBitsCmp{(a & nft::pageMask) <=> (b & nft::pageMask)}; lowBitsCmp != 0)
189 return lowBitsCmp < 0;
190
191 return a < b;
192}
193
194TER
195changeTokenURI(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, std::optional<xrpl::Slice> const& uri)
196{
197 std::shared_ptr<SLE> const page = locatePage(view, owner, nftokenID);
198
199 // If the page couldn't be found, the given NFT isn't owned by this account
200 if (!page)
201 return tecINTERNAL; // LCOV_EXCL_LINE
202
203 // Locate the NFT in the page
204 STArray& arr = page->peekFieldArray(sfNFTokens);
205
206 auto const nftIter = std::find_if(
207 arr.begin(), arr.end(), [&nftokenID](STObject const& obj) { return (obj[sfNFTokenID] == nftokenID); });
208
209 if (nftIter == arr.end())
210 return tecINTERNAL; // LCOV_EXCL_LINE
211
212 if (uri)
213 nftIter->setFieldVL(sfURI, *uri);
214 else if (nftIter->isFieldPresent(sfURI))
215 nftIter->makeFieldAbsent(sfURI);
216
217 view.update(page);
218 return tesSUCCESS;
219}
220
222TER
224{
225 XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token");
226
227 // First, we need to locate the page the NFT belongs to, creating it
228 // if necessary. This operation may fail if it is impossible to insert
229 // the NFT.
231 getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
232 adjustOwnerCount(view, view.peek(keylet::account(owner)), 1, beast::Journal{beast::Journal::getNullSink()});
233 });
234
235 if (!page)
237
238 {
239 auto arr = page->getFieldArray(sfNFTokens);
240 arr.push_back(std::move(nft));
241
242 arr.sort([](STObject const& o1, STObject const& o2) {
243 return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID));
244 });
245
246 page->setFieldArray(sfNFTokens, arr);
247 }
248
249 view.update(page);
250
251 return tesSUCCESS;
252}
253
254static bool
256{
257 if (p1->key() >= p2->key())
258 Throw<std::runtime_error>("mergePages: pages passed in out of order!");
259
260 if ((*p1)[~sfNextPageMin] != p2->key())
261 Throw<std::runtime_error>("mergePages: next link broken!");
262
263 if ((*p2)[~sfPreviousPageMin] != p1->key())
264 Throw<std::runtime_error>("mergePages: previous link broken!");
265
266 auto const p1arr = p1->getFieldArray(sfNFTokens);
267 auto const p2arr = p2->getFieldArray(sfNFTokens);
268
269 // Now check whether to merge the two pages; it only makes sense to do
270 // this it would mean that one of them can be deleted as a result of
271 // the merge.
272
273 if (p1arr.size() + p2arr.size() > dirMaxTokensPerPage)
274 return false;
275
276 STArray x(p1arr.size() + p2arr.size());
277
279 p1arr.begin(),
280 p1arr.end(),
281 p2arr.begin(),
282 p2arr.end(),
284 [](STObject const& a, STObject const& b) {
285 return compareTokens(a.getFieldH256(sfNFTokenID), b.getFieldH256(sfNFTokenID));
286 });
287
288 p2->setFieldArray(sfNFTokens, x);
289
290 // So, at this point we need to unlink "p1" (since we just emptied it) but
291 // we need to first relink the directory: if p1 has a previous page (p0),
292 // load it, point it to p2 and point p2 to it.
293
294 p2->makeFieldAbsent(sfPreviousPageMin);
295
296 if (auto const ppm = (*p1)[~sfPreviousPageMin])
297 {
298 auto p0 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm));
299
300 if (!p0)
301 Throw<std::runtime_error>("mergePages: p0 can't be located!");
302
303 p0->setFieldH256(sfNextPageMin, p2->key());
304 view.update(p0);
305
306 p2->setFieldH256(sfPreviousPageMin, *ppm);
307 }
308
309 view.update(p2);
310 view.erase(p1);
311
312 return true;
313}
314
316TER
317removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
318{
319 std::shared_ptr<SLE> page = locatePage(view, owner, nftokenID);
320
321 // If the page couldn't be found, the given NFT isn't owned by this account
322 if (!page)
323 return tecNO_ENTRY;
324
325 return removeToken(view, owner, nftokenID, std::move(page));
326}
327
329TER
330removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, std::shared_ptr<SLE>&& curr)
331{
332 // We found a page, but the given NFT may not be in it.
333 auto arr = curr->getFieldArray(sfNFTokens);
334
335 {
336 auto x = std::find_if(
337 arr.begin(), arr.end(), [&nftokenID](STObject const& obj) { return (obj[sfNFTokenID] == nftokenID); });
338
339 if (x == arr.end())
340 return tecNO_ENTRY;
341
342 arr.erase(x);
343 }
344
345 // Page management:
346 auto const loadPage = [&view](std::shared_ptr<SLE> const& page1, SF_UINT256 const& field) {
348
349 if (auto const id = (*page1)[~field])
350 {
351 page2 = view.peek(Keylet(ltNFTOKEN_PAGE, *id));
352
353 if (!page2)
354 Throw<std::runtime_error>(
355 "page " + to_string(page1->key()) + " has a broken " + field.getName() + " field pointing to " +
356 to_string(*id));
357 }
358
359 return page2;
360 };
361
362 auto const prev = loadPage(curr, sfPreviousPageMin);
363 auto const next = loadPage(curr, sfNextPageMin);
364
365 if (!arr.empty())
366 {
367 // The current page isn't empty. Update it and then try to consolidate
368 // pages. Note that this consolidation attempt may actually merge three
369 // pages into one!
370 curr->setFieldArray(sfNFTokens, arr);
371 view.update(curr);
372
373 int cnt = 0;
374
375 if (prev && mergePages(view, prev, curr))
376 cnt--;
377
378 if (next && mergePages(view, curr, next))
379 cnt--;
380
381 if (cnt != 0)
383 view, view.peek(keylet::account(owner)), cnt, beast::Journal{beast::Journal::getNullSink()});
384
385 return tesSUCCESS;
386 }
387
388 if (prev)
389 {
390 // With fixNFTokenPageLinks...
391 // The page is empty and there is a prev. If the last page of the
392 // directory is empty then we need to:
393 // 1. Move the contents of the previous page into the last page.
394 // 2. Fix up the link from prev's previous page.
395 // 3. Fix up the owner count.
396 // 4. Erase the previous page.
397 if (view.rules().enabled(fixNFTokenPageLinks) && ((curr->key() & nft::pageMask) == pageMask))
398 {
399 // Copy all relevant information from prev to curr.
400 curr->peekFieldArray(sfNFTokens) = prev->peekFieldArray(sfNFTokens);
401
402 if (auto const prevLink = prev->at(~sfPreviousPageMin))
403 {
404 curr->at(sfPreviousPageMin) = *prevLink;
405
406 // Also fix up the NextPageMin link in the new Previous.
407 auto const newPrev = loadPage(curr, sfPreviousPageMin);
408 newPrev->at(sfNextPageMin) = curr->key();
409 view.update(newPrev);
410 }
411 else
412 {
413 curr->makeFieldAbsent(sfPreviousPageMin);
414 }
415
417 view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()});
418
419 view.update(curr);
420 view.erase(prev);
421 return tesSUCCESS;
422 }
423
424 // The page is empty and not the last page, so we can just unlink it
425 // and then remove it.
426 if (next)
427 prev->setFieldH256(sfNextPageMin, next->key());
428 else
429 prev->makeFieldAbsent(sfNextPageMin);
430
431 view.update(prev);
432 }
433
434 if (next)
435 {
436 // Make our next page point to our previous page:
437 if (prev)
438 next->setFieldH256(sfPreviousPageMin, prev->key());
439 else
440 next->makeFieldAbsent(sfPreviousPageMin);
441
442 view.update(next);
443 }
444
445 view.erase(curr);
446
447 int cnt = 1;
448
449 // Since we're here, try to consolidate the previous and current pages
450 // of the page we removed (if any) into one. mergePages() _should_
451 // always return false. Since tokens are burned one at a time, there
452 // should never be a page containing one token sitting between two pages
453 // that have few enough tokens that they can be merged.
454 //
455 // But, in case that analysis is wrong, it's good to leave this code here
456 // just in case.
457 if (prev && next &&
459 view, view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())), view.peek(Keylet(ltNFTOKEN_PAGE, next->key()))))
460 cnt++;
461
462 adjustOwnerCount(view, view.peek(keylet::account(owner)), -1 * cnt, beast::Journal{beast::Journal::getNullSink()});
463
464 return tesSUCCESS;
465}
466
468findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID)
469{
470 std::shared_ptr<SLE const> page = locatePage(view, owner, nftokenID);
471
472 // If the page couldn't be found, the given NFT isn't owned by this account
473 if (!page)
474 return std::nullopt;
475
476 // We found a candidate page, but the given NFT may not be in it.
477 for (auto const& t : page->getFieldArray(sfNFTokens))
478 {
479 if (t[sfNFTokenID] == nftokenID)
480 return t;
481 }
482
483 return std::nullopt;
484}
485
487findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
488{
489 std::shared_ptr<SLE> page = locatePage(view, owner, nftokenID);
490
491 // If the page couldn't be found, the given NFT isn't owned by this account
492 if (!page)
493 return std::nullopt;
494
495 // We found a candidate page, but the given NFT may not be in it.
496 for (auto const& t : page->getFieldArray(sfNFTokens))
497 {
498 if (t[sfNFTokenID] == nftokenID)
499 // This std::optional constructor is explicit, so it is spelled out.
500 return std::optional<TokenAndPage>(std::in_place, t, std::move(page));
501 }
502 return std::nullopt;
503}
504
506removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t maxDeletableOffers)
507{
508 if (maxDeletableOffers == 0)
509 return 0;
510
511 std::optional<std::uint64_t> pageIndex{0};
512 std::size_t deletedOffersCount = 0;
513
514 do
515 {
516 auto const page = view.peek(keylet::page(directory, *pageIndex));
517 if (!page)
518 break;
519
520 // We get the index of the next page in case the current
521 // page is deleted after all of its entries have been removed
522 pageIndex = (*page)[~sfIndexNext];
523
524 auto offerIndexes = page->getFieldV256(sfIndexes);
525
526 // We reverse-iterate the offer directory page to delete all entries.
527 // Deleting an entry in a NFTokenOffer directory page won't cause
528 // entries from other pages to move to the current, so, it is safe to
529 // delete entries one by one in the page. It is required to iterate
530 // backwards to handle iterator invalidation for vector, as we are
531 // deleting during iteration.
532 for (int i = offerIndexes.size() - 1; i >= 0; --i)
533 {
534 if (auto const offer = view.peek(keylet::nftoffer(offerIndexes[i])))
535 {
536 if (deleteTokenOffer(view, offer))
537 ++deletedOffersCount;
538 else
539 Throw<std::runtime_error>("Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
540 }
541
542 if (maxDeletableOffers == deletedOffersCount)
543 break;
544 }
545 } while (pageIndex.value_or(0) && maxDeletableOffers != deletedOffersCount);
546
547 return deletedOffersCount;
548}
549
550TER
551notTooManyOffers(ReadView const& view, uint256 const& nftokenID)
552{
553 std::size_t totalOffers = 0;
554
555 {
556 Dir buys(view, keylet::nft_buys(nftokenID));
557 for (auto iter = buys.begin(); iter != buys.end(); iter.next_page())
558 {
559 totalOffers += iter.page_size();
560 if (totalOffers > maxDeletableTokenOfferEntries)
561 return tefTOO_BIG;
562 }
563 }
564
565 {
566 Dir sells(view, keylet::nft_sells(nftokenID));
567 for (auto iter = sells.begin(); iter != sells.end(); iter.next_page())
568 {
569 totalOffers += iter.page_size();
570 if (totalOffers > maxDeletableTokenOfferEntries)
571 return tefTOO_BIG;
572 }
573 }
574 return tesSUCCESS;
575}
576
577bool
579{
580 if (offer->getType() != ltNFTOKEN_OFFER)
581 return false;
582
583 auto const owner = (*offer)[sfOwner];
584
585 if (!view.dirRemove(keylet::ownerDir(owner), (*offer)[sfOwnerNode], offer->key(), false))
586 return false;
587
588 auto const nftokenID = (*offer)[sfNFTokenID];
589
590 if (!view.dirRemove(
591 ((*offer)[sfFlags] & tfSellNFToken) ? keylet::nft_sells(nftokenID) : keylet::nft_buys(nftokenID),
592 (*offer)[sfNFTokenOfferNode],
593 offer->key(),
594 false))
595 return false;
596
597 adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()});
598
599 view.erase(offer);
600 return true;
601}
602
603bool
605{
606 bool didRepair = false;
607
608 auto const last = keylet::nftpage_max(owner);
609
610 std::shared_ptr<SLE> page = view.peek(
611 Keylet(ltNFTOKEN_PAGE, view.succ(keylet::nftpage_min(owner).key, last.key.next()).value_or(last.key)));
612
613 if (!page)
614 return didRepair;
615
616 if (page->key() == last.key)
617 {
618 // There's only one page in this entire directory. There should be
619 // no links on that page.
620 bool const nextPresent = page->isFieldPresent(sfNextPageMin);
621 bool const prevPresent = page->isFieldPresent(sfPreviousPageMin);
622 if (nextPresent || prevPresent)
623 {
624 didRepair = true;
625 if (prevPresent)
626 page->makeFieldAbsent(sfPreviousPageMin);
627 if (nextPresent)
628 page->makeFieldAbsent(sfNextPageMin);
629 view.update(page);
630 }
631 return didRepair;
632 }
633
634 // First page is not the same as last page. The first page should not
635 // contain a previous link.
636 if (page->isFieldPresent(sfPreviousPageMin))
637 {
638 didRepair = true;
639 page->makeFieldAbsent(sfPreviousPageMin);
640 view.update(page);
641 }
642
643 std::shared_ptr<SLE> nextPage;
644 while (
645 (nextPage =
646 view.peek(Keylet(ltNFTOKEN_PAGE, view.succ(page->key().next(), last.key.next()).value_or(last.key)))))
647 {
648 if (!page->isFieldPresent(sfNextPageMin) || page->getFieldH256(sfNextPageMin) != nextPage->key())
649 {
650 didRepair = true;
651 page->setFieldH256(sfNextPageMin, nextPage->key());
652 view.update(page);
653 }
654
655 if (!nextPage->isFieldPresent(sfPreviousPageMin) || nextPage->getFieldH256(sfPreviousPageMin) != page->key())
656 {
657 didRepair = true;
658 nextPage->setFieldH256(sfPreviousPageMin, page->key());
659 view.update(nextPage);
660 }
661
662 if (nextPage->key() == last.key)
663 // We need special handling for the last page.
664 break;
665
666 page = nextPage;
667 }
668
669 // When we arrive here, nextPage should have the same index as last.
670 // If not, then that's something we need to fix.
671 if (!nextPage)
672 {
673 // It turns out that page is the last page for this owner, but
674 // that last page does not have the expected final index. We need
675 // to move the contents of the current last page into a page with the
676 // correct index.
677 //
678 // The owner count does not need to change because, even though
679 // we're adding a page, we'll also remove the page that used to be
680 // last.
681 didRepair = true;
682 nextPage = std::make_shared<SLE>(last);
683
684 // Copy all relevant information from prev to curr.
685 nextPage->peekFieldArray(sfNFTokens) = page->peekFieldArray(sfNFTokens);
686
687 if (auto const prevLink = page->at(~sfPreviousPageMin))
688 {
689 nextPage->at(sfPreviousPageMin) = *prevLink;
690
691 // Also fix up the NextPageMin link in the new Previous.
692 auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink));
693 if (!newPrev)
694 Throw<std::runtime_error>(
695 "NFTokenPage directory for " + to_string(owner) + " cannot be repaired. Unexpected link problem.");
696 newPrev->at(sfNextPageMin) = nextPage->key();
697 view.update(newPrev);
698 }
699 view.erase(page);
700 view.insert(nextPage);
701 return didRepair;
702 }
703
704 XRPL_ASSERT(nextPage, "xrpl::nft::repairNFTokenDirectoryLinks : next page is available");
705 if (nextPage->isFieldPresent(sfNextPageMin))
706 {
707 didRepair = true;
708 nextPage->makeFieldAbsent(sfNextPageMin);
709 view.update(nextPage);
710 }
711 return didRepair;
712}
713
714NotTEC
716 AccountID const& acctID,
717 STAmount const& amount,
718 std::optional<AccountID> const& dest,
719 std::optional<std::uint32_t> const& expiration,
720 std::uint16_t nftFlags,
721 Rules const& rules,
722 std::optional<AccountID> const& owner,
723 std::uint32_t txFlags)
724{
725 if (amount.negative())
726 // An offer for a negative amount makes no sense.
727 return temBAD_AMOUNT;
728
729 if (!isXRP(amount))
730 {
731 if (nftFlags & nft::flagOnlyXRP)
732 return temBAD_AMOUNT;
733
734 if (!amount)
735 return temBAD_AMOUNT;
736 }
737
738 // If this is an offer to buy, you must offer something; if it's an
739 // offer to sell, you can ask for nothing.
740 bool const isSellOffer = txFlags & tfSellNFToken;
741 if (!isSellOffer && !amount)
742 return temBAD_AMOUNT;
743
744 if (expiration.has_value() && expiration.value() == 0)
745 return temBAD_EXPIRATION;
746
747 // The 'Owner' field must be present when offering to buy, but can't
748 // be present when selling (it's implicit):
749 if (owner.has_value() == isSellOffer)
750 return temMALFORMED;
751
752 if (owner && owner == acctID)
753 return temMALFORMED;
754
755 // The destination can't be the account executing the transaction.
756 if (dest && dest == acctID)
757 {
758 return temMALFORMED;
759 }
760 return tesSUCCESS;
761}
762
763TER
765 ReadView const& view,
766 AccountID const& acctID,
767 AccountID const& nftIssuer,
768 STAmount const& amount,
769 std::optional<AccountID> const& dest,
770 std::uint16_t nftFlags,
771 std::uint16_t xferFee,
773 std::optional<AccountID> const& owner,
774 std::uint32_t txFlags)
775{
776 if (!(nftFlags & nft::flagCreateTrustLines) && !amount.native() && xferFee)
777 {
778 if (!view.exists(keylet::account(nftIssuer)))
779 return tecNO_ISSUER;
780
781 // If the IOU issuer and the NFToken issuer are the same, then that
782 // issuer does not need a trust line to accept their fee.
783 if (view.rules().enabled(featureNFTokenMintOffer))
784 {
785 if (nftIssuer != amount.getIssuer() && !view.read(keylet::line(nftIssuer, amount.issue())))
786 return tecNO_LINE;
787 }
788 else if (!view.exists(keylet::line(nftIssuer, amount.issue())))
789 {
790 return tecNO_LINE;
791 }
792
793 if (isFrozen(view, nftIssuer, amount.getCurrency(), amount.getIssuer()))
794 return tecFROZEN;
795 }
796
797 if (nftIssuer != acctID && !(nftFlags & nft::flagTransferable))
798 {
799 auto const root = view.read(keylet::account(nftIssuer));
800 XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
801
802 if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
804 }
805
806 if (isFrozen(view, acctID, amount.getCurrency(), amount.getIssuer()))
807 return tecFROZEN;
808
809 // If this is an offer to buy the token, the account must have the
810 // needed funds at hand; but note that funds aren't reserved and the
811 // offer may later become unfunded.
812 if ((txFlags & tfSellNFToken) == 0)
813 {
814 // We allow an IOU issuer to make a buy offer
815 // using their own currency.
816 if (accountFunds(view, acctID, amount, FreezeHandling::fhZERO_IF_FROZEN, j).signum() <= 0)
817 return tecUNFUNDED_OFFER;
818 }
819
820 if (dest)
821 {
822 // If a destination is specified, the destination must already be in
823 // the ledger.
824 auto const sleDst = view.read(keylet::account(*dest));
825
826 if (!sleDst)
827 return tecNO_DST;
828
829 // check if the destination has disallowed incoming offers
830 if (sleDst->getFlags() & lsfDisallowIncomingNFTokenOffer)
831 return tecNO_PERMISSION;
832 }
833
834 if (owner)
835 {
836 auto const sleOwner = view.read(keylet::account(*owner));
837
838 // defensively check
839 // it should not be possible to specify owner that doesn't exist
840 if (!sleOwner)
841 return tecNO_TARGET;
842
843 if (sleOwner->getFlags() & lsfDisallowIncomingNFTokenOffer)
844 return tecNO_PERMISSION;
845 }
846
847 if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native())
848 {
849 // If this is a sell offer, check that the account is allowed to
850 // receive IOUs. If this is a buy offer, we have to check that trustline
851 // is authorized, even though we previously checked it's balance via
852 // accountHolds. This is due to a possibility of existence of
853 // unauthorized trustlines with balance
854 auto const res = nft::checkTrustlineAuthorized(view, acctID, j, amount.asset().get<Issue>());
855 if (res != tesSUCCESS)
856 return res;
857 }
858 return tesSUCCESS;
859}
860
861TER
863 ApplyView& view,
864 AccountID const& acctID,
865 STAmount const& amount,
866 std::optional<AccountID> const& dest,
867 std::optional<std::uint32_t> const& expiration,
868 SeqProxy seqProxy,
869 uint256 const& nftokenID,
870 XRPAmount const& priorBalance,
872 std::uint32_t txFlags)
873{
874 Keylet const acctKeylet = keylet::account(acctID);
875 if (auto const acct = view.read(acctKeylet); priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1))
877
878 auto const offerID = keylet::nftoffer(acctID, seqProxy.value());
879
880 // Create the offer:
881 {
882 // Token offers are always added to the owner's owner directory:
883 auto const ownerNode = view.dirInsert(keylet::ownerDir(acctID), offerID, describeOwnerDir(acctID));
884
885 if (!ownerNode)
886 return tecDIR_FULL; // LCOV_EXCL_LINE
887
888 bool const isSellOffer = txFlags & tfSellNFToken;
889
890 // Token offers are also added to the token's buy or sell offer
891 // directory
892 auto const offerNode = view.dirInsert(
893 isSellOffer ? keylet::nft_sells(nftokenID) : keylet::nft_buys(nftokenID),
894 offerID,
895 [&nftokenID, isSellOffer](std::shared_ptr<SLE> const& sle) {
896 (*sle)[sfFlags] = isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
897 (*sle)[sfNFTokenID] = nftokenID;
898 });
899
900 if (!offerNode)
901 return tecDIR_FULL; // LCOV_EXCL_LINE
902
903 std::uint32_t sleFlags = 0;
904
905 if (isSellOffer)
906 sleFlags |= lsfSellNFToken;
907
908 auto offer = std::make_shared<SLE>(offerID);
909 (*offer)[sfOwner] = acctID;
910 (*offer)[sfNFTokenID] = nftokenID;
911 (*offer)[sfAmount] = amount;
912 (*offer)[sfFlags] = sleFlags;
913 (*offer)[sfOwnerNode] = *ownerNode;
914 (*offer)[sfNFTokenOfferNode] = *offerNode;
915
916 if (expiration)
917 (*offer)[sfExpiration] = *expiration;
918
919 if (dest)
920 (*offer)[sfDestination] = *dest;
921
922 view.insert(offer);
923 }
924
925 // Update owner count.
926 adjustOwnerCount(view, view.peek(acctKeylet), 1, j);
927
928 return tesSUCCESS;
929}
930
931TER
932checkTrustlineAuthorized(ReadView const& view, AccountID const id, beast::Journal const j, Issue const& issue)
933{
934 // Only valid for custom currencies
935 XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineAuthorized : valid to check.");
936
937 if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
938 {
939 auto const issuerAccount = view.read(keylet::account(issue.account));
940 if (!issuerAccount)
941 {
942 JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
943 "receive IOUs from non-existent issuer: "
944 << to_string(issue.account);
945
946 return tecNO_ISSUER;
947 }
948
949 // An account can not create a trustline to itself, so no line can
950 // exist to be authorized. Additionally, an issuer can always accept
951 // its own issuance.
952 if (issue.account == id)
953 {
954 return tesSUCCESS;
955 }
956
957 if (issuerAccount->isFlag(lsfRequireAuth))
958 {
959 auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
960
961 if (!trustLine)
962 {
963 return tecNO_LINE;
964 }
965
966 // Entries have a canonical representation, determined by a
967 // lexicographical "greater than" comparison employing strict
968 // weak ordering. Determine which entry we need to access.
969 if (!trustLine->isFlag(id > issue.account ? lsfLowAuth : lsfHighAuth))
970 {
971 return tecNO_AUTH;
972 }
973 }
974 }
975
976 return tesSUCCESS;
977}
978
979TER
980checkTrustlineDeepFrozen(ReadView const& view, AccountID const id, beast::Journal const j, Issue const& issue)
981{
982 // Only valid for custom currencies
983 XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineDeepFrozen : valid to check.");
984
985 if (view.rules().enabled(featureDeepFreeze))
986 {
987 auto const issuerAccount = view.read(keylet::account(issue.account));
988 if (!issuerAccount)
989 {
990 JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
991 "receive IOUs from non-existent issuer: "
992 << to_string(issue.account);
993
994 return tecNO_ISSUER;
995 }
996
997 // An account can not create a trustline to itself, so no line can
998 // exist to be frozen. Additionally, an issuer can always accept its
999 // own issuance.
1000 if (issue.account == id)
1001 {
1002 return tesSUCCESS;
1003 }
1004
1005 auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
1006
1007 if (!trustLine)
1008 {
1009 return tesSUCCESS;
1010 }
1011
1012 // There's no difference which side enacted deep freeze, accepting
1013 // tokens shouldn't be possible.
1014 bool const deepFrozen = (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
1015
1016 if (deepFrozen)
1017 {
1018 return tecFROZEN;
1019 }
1020 }
1021
1022 return tesSUCCESS;
1023}
1024
1025} // namespace nft
1026} // namespace xrpl
T back_inserter(T... args)
A generic endpoint for log messages.
Definition Journal.h:41
Stream debug() const
Definition Journal.h:301
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:115
virtual void update(std::shared_ptr< SLE > const &sle)=0
Indicate changes to a peeked SLE.
bool dirRemove(Keylet const &directory, std::uint64_t page, uint256 const &key, bool keepRoot)
Remove an entry from a directory.
virtual void erase(std::shared_ptr< SLE > const &sle)=0
Remove a peeked SLE.
virtual void insert(std::shared_ptr< SLE > const &sle)=0
Insert a new state SLE.
std::optional< std::uint64_t > dirInsert(Keylet const &directory, uint256 const &key, std::function< void(std::shared_ptr< SLE > const &)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:284
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
const_iterator & next_page()
Definition Dir.cpp:82
A class that simplifies iterating ledger directory pages.
Definition Dir.h:22
const_iterator begin() const
Definition Dir.cpp:14
const_iterator end() const
Definition Dir.cpp:32
A currency issued by an account.
Definition Issue.h:14
Currency currency
Definition Issue.h:16
AccountID account
Definition Issue.h:17
A view into a ledger.
Definition ReadView.h:32
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual Fees const & fees() const =0
Returns the fees for the base ledger.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual std::optional< key_type > succ(key_type const &key, std::optional< key_type > const &last=std::nullopt) const =0
Return the key of the next state item.
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
Rules controlling protocol behavior.
Definition Rules.h:19
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:118
size_type size() const
Definition STArray.h:224
iterator begin()
Definition STArray.h:200
iterator erase(iterator pos)
Definition STArray.h:266
iterator end()
Definition STArray.h:206
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:606
A type that represents either a sequence value or a ticket value.
Definition SeqProxy.h:37
constexpr std::uint32_t value() const
Definition SeqProxy.h:63
base_uint next() const
Definition base_uint.h:426
T find_if_not(T... args)
T in_place
T is_same_v
T make_move_iterator(T... args)
T merge(T... args)
Keylet nftpage_max(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:360
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:375
Keylet nftpage(Keylet const &k, uint256 const &token)
Definition Indexes.cpp:368
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:325
Keylet nft_buys(uint256 const &id) noexcept
The directory of buy offers for the specified NFT.
Definition Indexes.cpp:381
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:214
Keylet nftpage_min(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:352
Keylet nft_sells(uint256 const &id) noexcept
The directory of sell offers for the specified NFT.
Definition Indexes.cpp:387
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:160
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:331
TER insertToken(ApplyView &view, AccountID owner, STObject &&nft)
Insert the token in the owner's token directory.
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)
Preclaim checks shared by NFTokenCreateOffer and NFTokenMint.
constexpr std::uint16_t const flagCreateTrustLines
Definition nft.h:36
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)
doApply implementation shared by NFTokenCreateOffer and NFTokenMint
constexpr std::uint16_t const flagOnlyXRP
Definition nft.h:35
TER changeTokenURI(ApplyView &view, AccountID const &owner, uint256 const &nftokenID, std::optional< xrpl::Slice > const &uri)
constexpr std::uint16_t const flagTransferable
Definition nft.h:37
uint256 constexpr pageMask(std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"))
std::optional< STObject > findToken(ReadView const &view, AccountID const &owner, uint256 const &nftokenID)
Finds the specified token in the owner's token directory.
TER removeToken(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
Remove the token from the owner's token directory.
TER checkTrustlineDeepFrozen(ReadView const &view, AccountID const id, beast::Journal const j, Issue const &issue)
bool deleteTokenOffer(ApplyView &view, std::shared_ptr< SLE > const &offer)
Deletes the given token offer.
TER checkTrustlineAuthorized(ReadView const &view, AccountID const id, beast::Journal const j, Issue const &issue)
bool compareTokens(uint256 const &a, uint256 const &b)
std::size_t removeTokenOffersWithLimit(ApplyView &view, Keylet const &directory, std::size_t maxDeletableOffers)
Delete up to a specified number of offers from the specified token offer directory.
static std::shared_ptr< SLE > getPageForToken(ApplyView &view, AccountID const &owner, uint256 const &id, std::function< void(ApplyView &, AccountID const &)> const &createCallback)
static bool mergePages(ApplyView &view, std::shared_ptr< SLE > const &p1, std::shared_ptr< SLE > const &p2)
std::optional< TokenAndPage > findTokenAndPage(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
TER notTooManyOffers(ReadView const &view, uint256 const &nftokenID)
Returns tesSUCCESS if NFToken has few enough offers that it can be burned.
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)
Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
static std::shared_ptr< SLE const > locatePage(ReadView const &view, AccountID const &owner, uint256 const &id)
bool repairNFTokenDirectoryLinks(ApplyView &view, AccountID const &owner)
Repairs the links in an NFTokenPage directory.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
std::size_t constexpr dirMaxTokensPerPage
The maximum number of items in an NFT page.
Definition Protocol.h:47
@ fhZERO_IF_FROZEN
Definition View.h:59
bool isXRP(AccountID const &c)
Definition AccountID.h:71
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
@ tefTOO_BIG
Definition TER.h:165
@ tefNFTOKEN_IS_NOT_TRANSFERABLE
Definition TER.h:167
Number root(Number f, unsigned d)
Definition Number.cpp:938
std::size_t constexpr maxDeletableTokenOfferEntries
The maximum number of offers in an offer directory for NFT to be burnable.
Definition Protocol.h:56
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
Definition View.cpp:515
void adjustOwnerCount(ApplyView &view, std::shared_ptr< SLE > const &sle, std::int32_t amount, beast::Journal j)
Adjust the owner count up or down.
Definition View.cpp:941
bool isFrozen(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer)
Definition View.cpp:194
std::function< void(SLE::ref)> describeOwnerDir(AccountID const &account)
Definition View.cpp:955
@ temBAD_EXPIRATION
Definition TER.h:72
@ temMALFORMED
Definition TER.h:68
@ temBAD_AMOUNT
Definition TER.h:70
@ tecDIR_FULL
Definition TER.h:269
@ tecNO_ENTRY
Definition TER.h:288
@ tecNO_TARGET
Definition TER.h:286
@ tecNO_AUTH
Definition TER.h:282
@ tecNO_SUITABLE_NFTOKEN_PAGE
Definition TER.h:303
@ tecINTERNAL
Definition TER.h:292
@ tecFROZEN
Definition TER.h:285
@ tecUNFUNDED_OFFER
Definition TER.h:266
@ tecNO_LINE
Definition TER.h:283
@ tecINSUFFICIENT_RESERVE
Definition TER.h:289
@ tecNO_PERMISSION
Definition TER.h:287
@ tecNO_ISSUER
Definition TER.h:281
@ tecNO_DST
Definition TER.h:272
@ lsfDisallowIncomingNFTokenOffer
@ lsfNFTokenBuyOffers
@ lsfRequireAuth
@ lsfLowDeepFreeze
@ lsfSellNFToken
@ lsfNFTokenSellOffers
@ lsfHighDeepFreeze
@ lsfHighAuth
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:211
@ tesSUCCESS
Definition TER.h:226
T has_value(T... args)
XRPAmount accountReserve(std::size_t ownerCount) const
Returns the account reserve given the owner count, in drops.
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21
A field with a type known at compile time.
Definition SField.h:302
T swap(T... args)
T value(T... args)