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