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