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