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