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