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>
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
262TER
264{
265 XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token");
266
267 // First, we need to locate the page the NFT belongs to, creating it
268 // if necessary. This operation may fail if it is impossible to insert
269 // the NFT.
270 SLE::pointer const page =
271 getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
273 view,
274 view.peek(keylet::account(owner)),
275 1,
276 beast::Journal{beast::Journal::getNullSink()});
277 });
278
279 if (!page)
281
282 {
283 auto arr = page->getFieldArray(sfNFTokens);
284 arr.pushBack(std::move(nft));
285
286 arr.sort([](STObject const& o1, STObject const& o2) {
287 return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID));
288 });
289
290 page->setFieldArray(sfNFTokens, arr);
291 }
292
293 view.update(page);
294
295 return tesSUCCESS;
296}
297
298static bool
300{
301 if (p1->key() >= p2->key())
302 Throw<std::runtime_error>("mergePages: pages passed in out of order!");
303
304 if ((*p1)[~sfNextPageMin] != p2->key())
305 Throw<std::runtime_error>("mergePages: next link broken!");
306
307 if ((*p2)[~sfPreviousPageMin] != p1->key())
308 Throw<std::runtime_error>("mergePages: previous link broken!");
309
310 auto const p1arr = p1->getFieldArray(sfNFTokens);
311 auto const p2arr = p2->getFieldArray(sfNFTokens);
312
313 // Now check whether to merge the two pages; it only makes sense to do
314 // this it would mean that one of them can be deleted as a result of
315 // the merge.
316
317 if (p1arr.size() + p2arr.size() > kDirMaxTokensPerPage)
318 return false;
319
320 STArray x(p1arr.size() + p2arr.size());
321
323 p1arr, p2arr, std::back_inserter(x), [](STObject const& a, STObject const& b) {
324 return compareTokens(a.getFieldH256(sfNFTokenID), b.getFieldH256(sfNFTokenID));
325 });
326
327 p2->setFieldArray(sfNFTokens, x);
328
329 // So, at this point we need to unlink "p1" (since we just emptied it) but
330 // we need to first relink the directory: if p1 has a previous page (p0),
331 // load it, point it to p2 and point p2 to it.
332
333 p2->makeFieldAbsent(sfPreviousPageMin);
334
335 if (auto const ppm = (*p1)[~sfPreviousPageMin])
336 {
337 auto p0 = view.peek(Keylet(ltNFTOKEN_PAGE, *ppm));
338
339 if (!p0)
340 Throw<std::runtime_error>("mergePages: p0 can't be located!");
341
342 p0->setFieldH256(sfNextPageMin, p2->key());
343 view.update(p0);
344
345 p2->setFieldH256(sfPreviousPageMin, *ppm);
346 }
347
348 view.update(p2);
349 view.erase(p1);
350
351 return true;
352}
353
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
368TER
369removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, SLE::ref curr)
370{
371 // We found a page, but the given NFT may not be in it.
372 auto arr = curr->getFieldArray(sfNFTokens);
373
374 {
375 auto x = std::ranges::find_if(
376 arr, [&nftokenID](STObject const& obj) { return (obj[sfNFTokenID] == nftokenID); });
377
378 if (x == arr.end())
379 return tecNO_ENTRY;
380
381 arr.erase(x);
382 }
383
384 // Page management:
385 auto const loadPage = [&view](SLE::ref page1, SF_UINT256 const& field) {
386 SLE::pointer page2;
387
388 if (auto const id = (*page1)[~field])
389 {
390 page2 = view.peek(Keylet(ltNFTOKEN_PAGE, *id));
391
392 if (!page2)
393 {
395 "page " + to_string(page1->key()) + " has a broken " + field.getName() +
396 " field pointing to " + to_string(*id));
397 }
398 }
399
400 return page2;
401 };
402
403 auto const prev = loadPage(curr, sfPreviousPageMin);
404 auto const next = loadPage(curr, sfNextPageMin);
405
406 if (!arr.empty())
407 {
408 // The current page isn't empty. Update it and then try to consolidate
409 // pages. Note that this consolidation attempt may actually merge three
410 // pages into one!
411 curr->setFieldArray(sfNFTokens, arr);
412 view.update(curr);
413
414 int cnt = 0;
415
416 if (prev && mergePages(view, prev, curr))
417 cnt--;
418
419 if (next && mergePages(view, curr, next))
420 cnt--;
421
422 if (cnt != 0)
423 {
425 view,
426 view.peek(keylet::account(owner)),
427 cnt,
428 beast::Journal{beast::Journal::getNullSink()});
429 }
430
431 return tesSUCCESS;
432 }
433
434 if (prev)
435 {
436 // With fixNFTokenPageLinks...
437 // The page is empty and there is a prev. If the last page of the
438 // directory is empty then we need to:
439 // 1. Move the contents of the previous page into the last page.
440 // 2. Fix up the link from prev's previous page.
441 // 3. Fix up the owner count.
442 // 4. Erase the previous page.
443 if (view.rules().enabled(fixNFTokenPageLinks) &&
444 ((curr->key() & nft::kPageMask) == kPageMask))
445 {
446 // Copy all relevant information from prev to curr.
447 curr->peekFieldArray(sfNFTokens) = prev->peekFieldArray(sfNFTokens);
448
449 if (auto const prevLink = prev->at(~sfPreviousPageMin))
450 {
451 curr->at(sfPreviousPageMin) = *prevLink;
452
453 // Also fix up the NextPageMin link in the new Previous.
454 auto const newPrev = loadPage(curr, sfPreviousPageMin);
455 newPrev->at(sfNextPageMin) = curr->key();
456 view.update(newPrev);
457 }
458 else
459 {
460 curr->makeFieldAbsent(sfPreviousPageMin);
461 }
462
464 view,
465 view.peek(keylet::account(owner)),
466 -1,
467 beast::Journal{beast::Journal::getNullSink()});
468
469 view.update(curr);
470 view.erase(prev);
471 return tesSUCCESS;
472 }
473
474 // The page is empty and not the last page, so we can just unlink it
475 // and then remove it.
476 if (next)
477 {
478 prev->setFieldH256(sfNextPageMin, next->key());
479 }
480 else
481 {
482 prev->makeFieldAbsent(sfNextPageMin);
483 }
484
485 view.update(prev);
486 }
487
488 if (next)
489 {
490 // Make our next page point to our previous page:
491 if (prev)
492 {
493 next->setFieldH256(sfPreviousPageMin, prev->key());
494 }
495 else
496 {
497 next->makeFieldAbsent(sfPreviousPageMin);
498 }
499
500 view.update(next);
501 }
502
503 view.erase(curr);
504
505 int cnt = 1;
506
507 // Since we're here, try to consolidate the previous and current pages
508 // of the page we removed (if any) into one. mergePages() _should_
509 // always return false. Since tokens are burned one at a time, there
510 // should never be a page containing one token sitting between two pages
511 // that have few enough tokens that they can be merged.
512 //
513 // But, in case that analysis is wrong, it's good to leave this code here
514 // just in case.
515 if (prev && next &&
517 view,
518 view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())),
519 view.peek(Keylet(ltNFTOKEN_PAGE, next->key()))))
520 cnt++;
521
523 view,
524 view.peek(keylet::account(owner)),
525 -1 * cnt,
526 beast::Journal{beast::Journal::getNullSink()});
527
528 return tesSUCCESS;
529}
530
532findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID)
533{
534 SLE::const_pointer const page = locatePage(view, owner, nftokenID);
535
536 // If the page couldn't be found, the given NFT isn't owned by this account
537 if (!page)
538 return std::nullopt;
539
540 // We found a candidate page, but the given NFT may not be in it.
541 for (auto const& t : page->getFieldArray(sfNFTokens))
542 {
543 if (t[sfNFTokenID] == nftokenID)
544 return t;
545 }
546
547 return std::nullopt;
548}
549
551findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID)
552{
553 SLE::pointer page = locatePage(view, owner, nftokenID);
554
555 // If the page couldn't be found, the given NFT isn't owned by this account
556 if (!page)
557 return std::nullopt;
558
559 // We found a candidate page, but the given NFT may not be in it.
560 for (auto const& t : page->getFieldArray(sfNFTokens))
561 {
562 if (t[sfNFTokenID] == nftokenID)
563 {
564 // This std::optional constructor is explicit, so it is spelled out.
565 return std::optional<TokenAndPage>(std::in_place, t, std::move(page));
566 }
567 }
568 return std::nullopt;
569}
570
573{
574 if (maxDeletableOffers == 0)
575 return 0;
576
577 std::optional<std::uint64_t> pageIndex{0};
578 std::size_t deletedOffersCount = 0;
579
580 do
581 {
582 auto const page = view.peek(keylet::page(directory, *pageIndex));
583 if (!page)
584 break;
585
586 // We get the index of the next page in case the current
587 // page is deleted after all of its entries have been removed
588 pageIndex = (*page)[~sfIndexNext];
589
590 auto offerIndexes = page->getFieldV256(sfIndexes);
591
592 // We reverse-iterate the offer directory page to delete all entries.
593 // Deleting an entry in a NFTokenOffer directory page won't cause
594 // entries from other pages to move to the current, so, it is safe to
595 // delete entries one by one in the page. It is required to iterate
596 // backwards to handle iterator invalidation for vector, as we are
597 // deleting during iteration.
598 for (int i = offerIndexes.size() - 1; i >= 0; --i)
599 {
600 if (auto const offer = view.peek(keylet::nftokenOffer(offerIndexes[i])))
601 {
602 if (deleteTokenOffer(view, offer))
603 {
604 ++deletedOffersCount;
605 }
606 else
607 {
609 "Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
610 }
611 }
612
613 if (maxDeletableOffers == deletedOffersCount)
614 break;
615 }
616 } while ((pageIndex.value_or(0) != 0u) && maxDeletableOffers != deletedOffersCount);
617
618 return deletedOffersCount;
619}
620
621bool
623{
624 if (offer->getType() != ltNFTOKEN_OFFER)
625 return false;
626
627 auto const owner = (*offer)[sfOwner];
628
629 if (!view.dirRemove(keylet::ownerDir(owner), (*offer)[sfOwnerNode], offer->key(), false))
630 return false;
631
632 auto const nftokenID = (*offer)[sfNFTokenID];
633
634 if (!view.dirRemove(
635 offer->isFlag(lsfSellNFToken) ? keylet::nftSells(nftokenID)
636 : keylet::nftBuys(nftokenID),
637 (*offer)[sfNFTokenOfferNode],
638 offer->key(),
639 false))
640 return false;
641
643 view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()});
644
645 view.erase(offer);
646 return true;
647}
648
649bool
651{
652 bool didRepair = false;
653
654 auto const last = keylet::nftokenPageMax(owner);
655
656 SLE::pointer page = view.peek(Keylet(
657 ltNFTOKEN_PAGE,
658 view.succ(keylet::nftokenPageMin(owner).key, last.key.next()).value_or(last.key)));
659
660 if (!page)
661 return didRepair;
662
663 if (page->key() == last.key)
664 {
665 // There's only one page in this entire directory. There should be
666 // no links on that page.
667 bool const nextPresent = page->isFieldPresent(sfNextPageMin);
668 bool const prevPresent = page->isFieldPresent(sfPreviousPageMin);
669 if (nextPresent || prevPresent)
670 {
671 didRepair = true;
672 if (prevPresent)
673 page->makeFieldAbsent(sfPreviousPageMin);
674 if (nextPresent)
675 page->makeFieldAbsent(sfNextPageMin);
676 view.update(page);
677 }
678 return didRepair;
679 }
680
681 // First page is not the same as last page. The first page should not
682 // contain a previous link.
683 if (page->isFieldPresent(sfPreviousPageMin))
684 {
685 didRepair = true;
686 page->makeFieldAbsent(sfPreviousPageMin);
687 view.update(page);
688 }
689
690 SLE::pointer nextPage;
691 while (
692 (nextPage = view.peek(Keylet(
693 ltNFTOKEN_PAGE, view.succ(page->key().next(), last.key.next()).value_or(last.key)))))
694 {
695 if (!page->isFieldPresent(sfNextPageMin) ||
696 page->getFieldH256(sfNextPageMin) != nextPage->key())
697 {
698 didRepair = true;
699 page->setFieldH256(sfNextPageMin, nextPage->key());
700 view.update(page);
701 }
702
703 if (!nextPage->isFieldPresent(sfPreviousPageMin) ||
704 nextPage->getFieldH256(sfPreviousPageMin) != page->key())
705 {
706 didRepair = true;
707 nextPage->setFieldH256(sfPreviousPageMin, page->key());
708 view.update(nextPage);
709 }
710
711 if (nextPage->key() == last.key)
712 {
713 // We need special handling for the last page.
714 break;
715 }
716
717 page = nextPage;
718 }
719
720 // When we arrive here, nextPage should have the same index as last.
721 // If not, then that's something we need to fix.
722 if (!nextPage)
723 {
724 // It turns out that page is the last page for this owner, but
725 // that last page does not have the expected final index. We need
726 // to move the contents of the current last page into a page with the
727 // correct index.
728 //
729 // The owner count does not need to change because, even though
730 // we're adding a page, we'll also remove the page that used to be
731 // last.
732 didRepair = true;
733 nextPage = std::make_shared<SLE>(last);
734
735 // Copy all relevant information from prev to curr.
736 nextPage->peekFieldArray(sfNFTokens) = page->peekFieldArray(sfNFTokens);
737
738 if (auto const prevLink = page->at(~sfPreviousPageMin))
739 {
740 nextPage->at(sfPreviousPageMin) = *prevLink;
741
742 // Also fix up the NextPageMin link in the new Previous.
743 auto const newPrev = view.peek(Keylet(ltNFTOKEN_PAGE, *prevLink));
744 if (!newPrev)
745 {
747 "NFTokenPage directory for " + to_string(owner) +
748 " cannot be repaired. Unexpected link problem.");
749 }
750 newPrev->at(sfNextPageMin) = nextPage->key();
751 view.update(newPrev);
752 }
753 view.erase(page);
754 view.insert(nextPage);
755 return didRepair;
756 }
757
758 XRPL_ASSERT(nextPage, "xrpl::nft::repairNFTokenDirectoryLinks : next page is available");
759 if (nextPage->isFieldPresent(sfNextPageMin))
760 {
761 didRepair = true;
762 nextPage->makeFieldAbsent(sfNextPageMin);
763 view.update(nextPage);
764 }
765 return didRepair;
766}
767
768NotTEC
770 AccountID const& acctID,
771 STAmount const& amount,
772 std::optional<AccountID> const& dest,
773 std::optional<std::uint32_t> const& expiration,
774 std::uint16_t nftFlags,
775 Rules const& rules,
776 std::optional<AccountID> const& owner,
777 std::uint32_t txFlags)
778{
779 if (amount.negative())
780 {
781 // An offer for a negative amount makes no sense.
782 return temBAD_AMOUNT;
783 }
784
785 if (!isXRP(amount))
786 {
787 if ((nftFlags & nft::kFlagOnlyXrp) != 0)
788 return temBAD_AMOUNT;
789
790 if (!amount)
791 return temBAD_AMOUNT;
792 }
793
794 // If this is an offer to buy, you must offer something; if it's an
795 // offer to sell, you can ask for nothing.
796 bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
797 if (!isSellOffer && !amount)
798 return temBAD_AMOUNT;
799
800 if (expiration.has_value() && expiration.value() == 0)
801 return temBAD_EXPIRATION;
802
803 // The 'Owner' field must be present when offering to buy, but can't
804 // be present when selling (it's implicit):
805 if (owner.has_value() == isSellOffer)
806 return temMALFORMED;
807
808 if (owner && owner == acctID)
809 return temMALFORMED;
810
811 // The destination can't be the account executing the transaction.
812 if (dest && dest == acctID)
813 {
814 return temMALFORMED;
815 }
816 return tesSUCCESS;
817}
818
819TER
821 ReadView const& view,
822 AccountID const& acctID,
823 AccountID const& nftIssuer,
824 STAmount const& amount,
825 std::optional<AccountID> const& dest,
826 std::uint16_t nftFlags,
827 std::uint16_t xferFee,
829 std::optional<AccountID> const& owner,
830 std::uint32_t txFlags)
831{
832 if (((nftFlags & nft::kFlagCreateTrustLines) == 0) && !amount.native() && (xferFee != 0u))
833 {
834 if (!view.exists(keylet::account(nftIssuer)))
835 return tecNO_ISSUER;
836
837 // If the IOU issuer and the NFToken issuer are the same, then that
838 // issuer does not need a trust line to accept their fee.
839 if (view.rules().enabled(featureNFTokenMintOffer))
840 {
841 if (nftIssuer != amount.getIssuer() &&
842 !view.read(keylet::trustLine(nftIssuer, amount.get<Issue>())))
843 return tecNO_LINE;
844 }
845 else if (!view.exists(keylet::trustLine(nftIssuer, amount.get<Issue>())))
846 {
847 return tecNO_LINE;
848 }
849
850 if (isFrozen(view, nftIssuer, amount.get<Issue>().currency, amount.getIssuer()))
851 return tecFROZEN;
852 }
853
854 if (nftIssuer != acctID && ((nftFlags & nft::kFlagTransferable) == 0))
855 {
856 auto const root = view.read(keylet::account(nftIssuer));
857 XRPL_ASSERT(root, "xrpl::nft::tokenOfferCreatePreclaim : non-null account");
858
859 if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
861 }
862
863 if (isFrozen(view, acctID, amount.get<Issue>().currency, amount.getIssuer()))
864 return tecFROZEN;
865
866 // If this is an offer to buy the token, the account must have the
867 // needed funds at hand; but note that funds aren't reserved and the
868 // offer may later become unfunded.
869 if ((txFlags & tfSellNFToken) == 0)
870 {
871 // We allow an IOU issuer to make a buy offer
872 // using their own currency.
873 if (accountFunds(view, acctID, amount, FreezeHandling::ZeroIfFrozen, j).signum() <= 0)
874 return tecUNFUNDED_OFFER;
875 }
876
877 if (dest)
878 {
879 // If a destination is specified, the destination must already be in
880 // the ledger.
881 auto const sleDst = view.read(keylet::account(*dest));
882
883 if (!sleDst)
884 return tecNO_DST;
885
886 // check if the destination has disallowed incoming offers
887 if (sleDst->isFlag(lsfDisallowIncomingNFTokenOffer))
888 return tecNO_PERMISSION;
889 }
890
891 if (owner)
892 {
893 auto const sleOwner = view.read(keylet::account(*owner));
894
895 // defensively check
896 // it should not be possible to specify owner that doesn't exist
897 if (!sleOwner)
898 return tecNO_TARGET;
899
900 if (sleOwner->isFlag(lsfDisallowIncomingNFTokenOffer))
901 return tecNO_PERMISSION;
902 }
903
904 if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native())
905 {
906 // If this is a sell offer, check that the account is allowed to
907 // receive IOUs. If this is a buy offer, we have to check that trustline
908 // is authorized, even though we previously checked it's balance via
909 // accountHolds. This is due to a possibility of existence of
910 // unauthorized trustlines with balance
911 auto const res =
912 nft::checkTrustlineAuthorized(view, acctID, j, amount.asset().get<Issue>());
913 if (!isTesSuccess(res))
914 return res;
915 }
916 return tesSUCCESS;
917}
918
919TER
921 ApplyView& view,
922 AccountID const& acctID,
923 STAmount const& amount,
924 std::optional<AccountID> const& dest,
925 std::optional<std::uint32_t> const& expiration,
926 SeqProxy seqProxy,
927 uint256 const& nftokenID,
928 XRPAmount const& priorBalance,
930 std::uint32_t txFlags)
931{
932 Keylet const acctKeylet = keylet::account(acctID);
933 if (auto const acct = view.read(acctKeylet);
934 priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1))
936
937 auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value());
938
939 // Create the offer:
940 {
941 // Token offers are always added to the owner's owner directory:
942 auto const ownerNode =
943 view.dirInsert(keylet::ownerDir(acctID), offerID, describeOwnerDir(acctID));
944
945 if (!ownerNode)
946 return tecDIR_FULL; // LCOV_EXCL_LINE
947
948 bool const isSellOffer = (txFlags & tfSellNFToken) != 0u;
949
950 // Token offers are also added to the token's buy or sell offer
951 // directory
952 auto const offerNode = view.dirInsert(
953 isSellOffer ? keylet::nftSells(nftokenID) : keylet::nftBuys(nftokenID),
954 offerID,
955 [&nftokenID, isSellOffer](SLE::ref sle) {
956 (*sle)[sfFlags] = isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
957 (*sle)[sfNFTokenID] = nftokenID;
958 });
959
960 if (!offerNode)
961 return tecDIR_FULL; // LCOV_EXCL_LINE
962
963 std::uint32_t sleFlags = 0;
964
965 if (isSellOffer)
966 sleFlags |= lsfSellNFToken;
967
968 auto offer = std::make_shared<SLE>(offerID);
969 (*offer)[sfOwner] = acctID;
970 (*offer)[sfNFTokenID] = nftokenID;
971 (*offer)[sfAmount] = amount;
972 (*offer)[sfFlags] = sleFlags;
973 (*offer)[sfOwnerNode] = *ownerNode;
974 (*offer)[sfNFTokenOfferNode] = *offerNode;
975
976 if (expiration)
977 (*offer)[sfExpiration] = *expiration;
978
979 if (dest)
980 (*offer)[sfDestination] = *dest;
981
982 view.insert(offer);
983 }
984
985 // Update owner count.
986 adjustOwnerCount(view, view.peek(acctKeylet), 1, j);
987
988 return tesSUCCESS;
989}
990
991TER
993 ReadView const& view,
994 AccountID const id,
995 beast::Journal const j,
996 Issue const& issue)
997{
998 // Only valid for custom currencies
999 XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineAuthorized : valid to check.");
1000
1001 if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
1002 {
1003 auto const issuerAccount = view.read(keylet::account(issue.account));
1004 if (!issuerAccount)
1005 {
1006 JLOG(j.debug()) << "xrpl::nft::checkTrustlineAuthorized: can't "
1007 "receive IOUs from non-existent issuer: "
1008 << to_string(issue.account);
1009
1010 return tecNO_ISSUER;
1011 }
1012
1013 // An account can not create a trustline to itself, so no line can
1014 // exist to be authorized. Additionally, an issuer can always accept
1015 // its own issuance.
1016 if (issue.account == id)
1017 {
1018 return tesSUCCESS;
1019 }
1020
1021 if (issuerAccount->isFlag(lsfRequireAuth))
1022 {
1023 auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
1024
1025 if (!trustLine)
1026 {
1027 return tecNO_LINE;
1028 }
1029
1030 // Entries have a canonical representation, determined by a
1031 // lexicographical "greater than" comparison employing strict
1032 // weak ordering. Determine which entry we need to access.
1033 if (!trustLine->isFlag(id > issue.account ? lsfLowAuth : lsfHighAuth))
1034 {
1035 return tecNO_AUTH;
1036 }
1037 }
1038 }
1039
1040 return tesSUCCESS;
1041}
1042
1043TER
1045 ReadView const& view,
1046 AccountID const id,
1047 beast::Journal const j,
1048 Issue const& issue)
1049{
1050 // Only valid for custom currencies
1051 XRPL_ASSERT(!isXRP(issue.currency), "xrpl::nft::checkTrustlineDeepFrozen : valid to check.");
1052
1053 if (view.rules().enabled(featureDeepFreeze))
1054 {
1055 auto const issuerAccount = view.read(keylet::account(issue.account));
1056 if (!issuerAccount)
1057 {
1058 JLOG(j.debug()) << "xrpl::nft::checkTrustlineDeepFrozen: can't "
1059 "receive IOUs from non-existent issuer: "
1060 << to_string(issue.account);
1061
1062 return tecNO_ISSUER;
1063 }
1064
1065 // An account can not create a trustline to itself, so no line can
1066 // exist to be frozen. Additionally, an issuer can always accept its
1067 // own issuance.
1068 if (issue.account == id)
1069 {
1070 return tesSUCCESS;
1071 }
1072
1073 auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
1074
1075 if (!trustLine)
1076 {
1077 return tesSUCCESS;
1078 }
1079
1080 // There's no difference which side enacted deep freeze, accepting
1081 // tokens shouldn't be possible.
1082 bool const deepFrozen =
1083 ((*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze)) != 0u;
1084
1085 if (deepFrozen)
1086 {
1087 return tecFROZEN;
1088 }
1089 }
1090
1091 return tesSUCCESS;
1092}
1093
1094} // namespace xrpl::nft
T back_inserter(T... args)
A generic endpoint for log messages.
Definition Journal.h:38
Stream debug() const
Definition Journal.h:297
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:118
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:340
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:460
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 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:33
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
constexpr TIss const & get() const
bool negative() const noexcept
Definition STAmount.h:466
bool native() const noexcept
Definition STAmount.h:453
Asset const & asset() const
Definition STAmount.h:478
AccountID const & getIssuer() const
Definition STAmount.h:498
size_type size() const
Definition STArray.h:240
iterator begin()
Definition STArray.h:216
iterator erase(iterator pos)
Definition STArray.h:282
iterator end()
Definition STArray.h:222
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:621
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
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 ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:357
Keylet nftokenPageMin(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:384
Keylet nftokenPage(Keylet const &k, uint256 const &token)
Definition Indexes.cpp:400
Keylet nftokenPageMax(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:392
Keylet nftSells(uint256 const &id) noexcept
The directory of sell offers for the specified NFT.
Definition Indexes.cpp:419
Keylet nftBuys(uint256 const &id) noexcept
The directory of buy offers for the specified NFT.
Definition Indexes.cpp:413
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:363
Keylet nftokenOffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:407
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:241
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:70
@ tefNFTOKEN_IS_NOT_TRANSFERABLE
Definition TER.h:176
Number root(Number f, unsigned d)
Definition Number.cpp:1201
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:594
void adjustOwnerCount(ApplyView &view, SLE::ref 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.
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:28
@ temBAD_EXPIRATION
Definition TER.h:77
@ temMALFORMED
Definition TER.h:73
@ temBAD_AMOUNT
Definition TER.h:75
constexpr std::size_t kDirMaxTokensPerPage
The maximum number of items in an NFT page.
Definition Protocol.h:50
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecDIR_FULL
Definition TER.h:285
@ tecNO_ENTRY
Definition TER.h:304
@ tecNO_TARGET
Definition TER.h:302
@ tecNO_AUTH
Definition TER.h:298
@ tecNO_SUITABLE_NFTOKEN_PAGE
Definition TER.h:319
@ tecINTERNAL
Definition TER.h:308
@ tecFROZEN
Definition TER.h:301
@ tecUNFUNDED_OFFER
Definition TER.h:282
@ tecNO_LINE
Definition TER.h:299
@ tecINSUFFICIENT_RESERVE
Definition TER.h:305
@ tecNO_PERMISSION
Definition TER.h:303
@ tecNO_ISSUER
Definition TER.h:297
@ tecNO_DST
Definition TER.h:288
BaseUInt< 256 > uint256
Definition base_uint.h:562
@ tesSUCCESS
Definition TER.h:240
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:49
TypedField< STBitString< 256 > > SF_UINT256
Definition SField.h:341
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
T swap(T... args)
T value(T... args)
T value_or(T... args)