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