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