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