rippled
Loading...
Searching...
No Matches
NFTokenBurn_test.cpp
1#include <test/jtx.h>
2
3#include <xrpld/app/tx/detail/NFTokenUtils.h>
4
5#include <xrpl/protocol/Feature.h>
6#include <xrpl/protocol/jss.h>
7
8#include <random>
9
10namespace ripple {
11
13{
14 // Helper function that returns the number of nfts owned by an account.
15 static std::uint32_t
17 {
18 Json::Value params;
19 params[jss::account] = acct.human();
20 params[jss::type] = "state";
21 Json::Value nfts = env.rpc("json", "account_nfts", to_string(params));
22 return nfts[jss::result][jss::account_nfts].size();
23 };
24
25 // Helper function that returns new nft id for an account and create
26 // specified number of sell offers
29 test::jtx::Env& env,
30 test::jtx::Account const& owner,
31 std::vector<uint256>& offerIndexes,
32 size_t const tokenCancelCount)
33 {
34 using namespace test::jtx;
35 uint256 const nftokenID =
36 token::getNextID(env, owner, 0, tfTransferable);
37 env(token::mint(owner, 0),
38 token::uri(std::string(maxTokenURILength, 'u')),
39 txflags(tfTransferable));
40 env.close();
41
42 offerIndexes.reserve(tokenCancelCount);
43
44 for (uint32_t i = 0; i < tokenCancelCount; ++i)
45 {
46 // Create sell offer
47 offerIndexes.push_back(keylet::nftoffer(owner, env.seq(owner)).key);
48 env(token::createOffer(owner, nftokenID, drops(1)),
49 txflags(tfSellNFToken));
50 env.close();
51 }
52
53 return nftokenID;
54 };
55
56 // printNFTPages is a helper function that may be used for debugging.
57 //
58 // It uses the ledger RPC command to show the NFT pages in the ledger.
59 // This parameter controls how noisy the output is.
60 enum Volume : bool {
61 quiet = false,
62 noisy = true,
63 };
64
65 void
67 {
68 Json::Value jvParams;
69 jvParams[jss::ledger_index] = "current";
70 jvParams[jss::binary] = false;
71 {
72 Json::Value jrr =
73 env.rpc("json", "ledger_data", to_string(jvParams));
74
75 // Iterate the state and print all NFTokenPages.
76 if (!jrr.isMember(jss::result) ||
77 !jrr[jss::result].isMember(jss::state))
78 {
79 std::cout << "No ledger state found!" << std::endl;
80 return;
81 }
82 Json::Value& state = jrr[jss::result][jss::state];
83 if (!state.isArray())
84 {
85 std::cout << "Ledger state is not array!" << std::endl;
86 return;
87 }
88 for (Json::UInt i = 0; i < state.size(); ++i)
89 {
90 if (state[i].isMember(sfNFTokens.jsonName) &&
91 state[i][sfNFTokens.jsonName].isArray())
92 {
93 std::uint32_t tokenCount =
94 state[i][sfNFTokens.jsonName].size();
95 std::cout << tokenCount << " NFtokens in page "
96 << state[i][jss::index].asString() << std::endl;
97
98 if (vol == noisy)
99 {
100 std::cout << state[i].toStyledString() << std::endl;
101 }
102 else
103 {
104 if (tokenCount > 0)
105 std::cout << "first: "
106 << state[i][sfNFTokens.jsonName][0u]
108 << std::endl;
109 if (tokenCount > 1)
111 << "last: "
112 << state[i][sfNFTokens.jsonName][tokenCount - 1]
114 << std::endl;
115 }
116 }
117 }
118 }
119 }
120
121 void
123 {
124 // Exercise a number of conditions with NFT burning.
125 testcase("Burn random");
126
127 using namespace test::jtx;
128
129 Env env{*this, features};
130
131 // Keep information associated with each account together.
132 struct AcctStat
133 {
134 test::jtx::Account const acct;
136
137 AcctStat(char const* name) : acct(name)
138 {
139 }
140
141 operator test::jtx::Account() const
142 {
143 return acct;
144 }
145 };
146 AcctStat alice{"alice"};
147 AcctStat becky{"becky"};
148 AcctStat minter{"minter"};
149
150 env.fund(XRP(10000), alice, becky, minter);
151 env.close();
152
153 // Both alice and minter mint nfts in case that makes any difference.
154 env(token::setMinter(alice, minter));
155 env.close();
156
157 // Create enough NFTs that alice, becky, and minter can all have
158 // at least three pages of NFTs. This will cause more activity in
159 // the page coalescing code. If we make 210 NFTs in total, we can
160 // have alice and minter each make 105. That will allow us to
161 // distribute 70 NFTs to our three participants.
162 //
163 // Give each NFT a pseudo-randomly chosen fee so the NFTs are
164 // distributed pseudo-randomly through the pages. This should
165 // prevent alice's and minter's NFTs from clustering together
166 // in becky's directory.
167 //
168 // Use a default initialized mercenne_twister because we want the
169 // effect of random numbers, but we want the test to run the same
170 // way each time.
171 std::mt19937 engine;
173 decltype(maxTransferFee){}, maxTransferFee);
174
175 alice.nfts.reserve(105);
176 while (alice.nfts.size() < 105)
177 {
178 std::uint16_t const xferFee = feeDist(engine);
179 alice.nfts.push_back(token::getNextID(
180 env, alice, 0u, tfTransferable | tfBurnable, xferFee));
181 env(token::mint(alice),
182 txflags(tfTransferable | tfBurnable),
183 token::xferFee(xferFee));
184 env.close();
185 }
186
187 minter.nfts.reserve(105);
188 while (minter.nfts.size() < 105)
189 {
190 std::uint16_t const xferFee = feeDist(engine);
191 minter.nfts.push_back(token::getNextID(
192 env, alice, 0u, tfTransferable | tfBurnable, xferFee));
193 env(token::mint(minter),
194 txflags(tfTransferable | tfBurnable),
195 token::xferFee(xferFee),
196 token::issuer(alice));
197 env.close();
198 }
199
200 // All of the NFTs are now minted. Transfer 35 each over to becky so
201 // we end up with 70 NFTs in each account.
202 becky.nfts.reserve(70);
203 {
204 auto aliceIter = alice.nfts.begin();
205 auto minterIter = minter.nfts.begin();
206 while (becky.nfts.size() < 70)
207 {
208 // We do the same work on alice and minter, so make a lambda.
209 auto xferNFT = [&env, &becky](AcctStat& acct, auto& iter) {
210 uint256 offerIndex =
211 keylet::nftoffer(acct.acct, env.seq(acct.acct)).key;
212 env(token::createOffer(acct, *iter, XRP(0)),
213 txflags(tfSellNFToken));
214 env.close();
215 env(token::acceptSellOffer(becky, offerIndex));
216 env.close();
217 becky.nfts.push_back(*iter);
218 iter = acct.nfts.erase(iter);
219 iter += 2;
220 };
221 xferNFT(alice, aliceIter);
222 xferNFT(minter, minterIter);
223 }
224 BEAST_EXPECT(aliceIter == alice.nfts.end());
225 BEAST_EXPECT(minterIter == minter.nfts.end());
226 }
227
228 // Now all three participants have 70 NFTs.
229 BEAST_EXPECT(nftCount(env, alice.acct) == 70);
230 BEAST_EXPECT(nftCount(env, becky.acct) == 70);
231 BEAST_EXPECT(nftCount(env, minter.acct) == 70);
232
233 // Next we'll create offers for all of those NFTs. This calls for
234 // another lambda.
235 auto addOffers =
236 [&env](AcctStat& owner, AcctStat& other1, AcctStat& other2) {
237 for (uint256 nft : owner.nfts)
238 {
239 // Create sell offers for owner.
240 env(token::createOffer(owner, nft, drops(1)),
241 txflags(tfSellNFToken),
242 token::destination(other1));
243 env(token::createOffer(owner, nft, drops(1)),
244 txflags(tfSellNFToken),
245 token::destination(other2));
246 env.close();
247
248 // Create buy offers for other1 and other2.
249 env(token::createOffer(other1, nft, drops(1)),
250 token::owner(owner));
251 env(token::createOffer(other2, nft, drops(1)),
252 token::owner(owner));
253 env.close();
254
255 env(token::createOffer(other2, nft, drops(2)),
256 token::owner(owner));
257 env(token::createOffer(other1, nft, drops(2)),
258 token::owner(owner));
259 env.close();
260 }
261 };
262 addOffers(alice, becky, minter);
263 addOffers(becky, minter, alice);
264 addOffers(minter, alice, becky);
265 BEAST_EXPECT(ownerCount(env, alice) == 424);
266 BEAST_EXPECT(ownerCount(env, becky) == 424);
267 BEAST_EXPECT(ownerCount(env, minter) == 424);
268
269 // Now each of the 270 NFTs has six offers associated with it.
270 // Randomly select an NFT out of the pile and burn it. Continue
271 // the process until all NFTs are burned.
272 AcctStat* const stats[3] = {&alice, &becky, &minter};
275
276 while (stats[0]->nfts.size() > 0 || stats[1]->nfts.size() > 0 ||
277 stats[2]->nfts.size() > 0)
278 {
279 // Pick an account to burn an nft. If there are no nfts left
280 // pick again.
281 AcctStat& owner = *(stats[acctDist(engine)]);
282 if (owner.nfts.empty())
283 continue;
284
285 // Pick one of the nfts.
287 0lu, owner.nfts.size() - 1);
288 auto nftIter = owner.nfts.begin() + nftDist(engine);
289 uint256 const nft = *nftIter;
290 owner.nfts.erase(nftIter);
291
292 // Decide which of the accounts should burn the nft. If the
293 // owner is becky then any of the three accounts can burn.
294 // Otherwise either alice or minter can burn.
295 AcctStat& burner = owner.acct == becky.acct
296 ? *(stats[acctDist(engine)])
297 : mintDist(engine) ? alice
298 : minter;
299
300 if (owner.acct == burner.acct)
301 env(token::burn(burner, nft));
302 else
303 env(token::burn(burner, nft), token::owner(owner));
304 env.close();
305
306 // Every time we burn an nft, the number of nfts they hold should
307 // match the number of nfts we think they hold.
308 BEAST_EXPECT(nftCount(env, alice.acct) == alice.nfts.size());
309 BEAST_EXPECT(nftCount(env, becky.acct) == becky.nfts.size());
310 BEAST_EXPECT(nftCount(env, minter.acct) == minter.nfts.size());
311 }
312 BEAST_EXPECT(nftCount(env, alice.acct) == 0);
313 BEAST_EXPECT(nftCount(env, becky.acct) == 0);
314 BEAST_EXPECT(nftCount(env, minter.acct) == 0);
315
316 // When all nfts are burned none of the accounts should have
317 // an ownerCount.
318 BEAST_EXPECT(ownerCount(env, alice) == 0);
319 BEAST_EXPECT(ownerCount(env, becky) == 0);
320 BEAST_EXPECT(ownerCount(env, minter) == 0);
321 }
322
323 void
325 {
326 // The earlier burn test randomizes which nft is burned. There are
327 // a couple of directory merging scenarios that can only be tested by
328 // inserting and deleting in an ordered fashion. We do that testing
329 // now.
330 testcase("Burn sequential");
331
332 using namespace test::jtx;
333
334 Account const alice{"alice"};
335
336 Env env{*this, features};
337 env.fund(XRP(1000), alice);
338
339 // A lambda that generates 96 nfts packed into three pages of 32 each.
340 // Returns a sorted vector of the NFTokenIDs packed into the pages.
341 auto genPackedTokens = [this, &env, &alice]() {
343 nfts.reserve(96);
344
345 // We want to create fully packed NFT pages. This is a little
346 // tricky since the system currently in place is inclined to
347 // assign consecutive tokens to only 16 entries per page.
348 //
349 // By manipulating the internal form of the taxon we can force
350 // creation of NFT pages that are completely full. This lambda
351 // tells us the taxon value we should pass in in order for the
352 // internal representation to match the passed in value.
353 auto internalTaxon = [&env](
354 Account const& acct,
355 std::uint32_t taxon) -> std::uint32_t {
356 std::uint32_t tokenSeq =
357 env.le(acct)->at(~sfMintedNFTokens).value_or(0);
358
359 // We must add FirstNFTokenSequence.
360 tokenSeq += env.le(acct)
361 ->at(~sfFirstNFTokenSequence)
362 .value_or(env.seq(acct));
363
364 return toUInt32(
365 nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
366 };
367
368 for (std::uint32_t i = 0; i < 96; ++i)
369 {
370 // In order to fill the pages we use the taxon to break them
371 // into groups of 16 entries. By having the internal
372 // representation of the taxon go...
373 // 0, 3, 2, 5, 4, 7...
374 // in sets of 16 NFTs we can get each page to be fully
375 // populated.
376 std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
377 uint32_t const extTaxon = internalTaxon(alice, intTaxon);
378 nfts.push_back(token::getNextID(env, alice, extTaxon));
379 env(token::mint(alice, extTaxon));
380 env.close();
381 }
382
383 // Sort the NFTs so they are listed in storage order, not
384 // creation order.
385 std::sort(nfts.begin(), nfts.end());
386
387 // Verify that the ledger does indeed contain exactly three pages
388 // of NFTs with 32 entries in each page.
389 Json::Value jvParams;
390 jvParams[jss::ledger_index] = "current";
391 jvParams[jss::binary] = false;
392 {
393 Json::Value jrr =
394 env.rpc("json", "ledger_data", to_string(jvParams));
395
396 Json::Value& state = jrr[jss::result][jss::state];
397
398 int pageCount = 0;
399 for (Json::UInt i = 0; i < state.size(); ++i)
400 {
401 if (state[i].isMember(sfNFTokens.jsonName) &&
402 state[i][sfNFTokens.jsonName].isArray())
403 {
404 BEAST_EXPECT(
405 state[i][sfNFTokens.jsonName].size() == 32);
406 ++pageCount;
407 }
408 }
409 // If this check fails then the internal NFT directory logic
410 // has changed.
411 BEAST_EXPECT(pageCount == 3);
412 }
413 return nfts;
414 };
415 {
416 // Generate three packed pages. Then burn the tokens in order from
417 // first to last. This exercises specific cases where coalescing
418 // pages is not possible.
419 std::vector<uint256> nfts = genPackedTokens();
420 BEAST_EXPECT(nftCount(env, alice) == 96);
421 BEAST_EXPECT(ownerCount(env, alice) == 3);
422
423 for (uint256 const& nft : nfts)
424 {
425 env(token::burn(alice, {nft}));
426 env.close();
427 }
428 BEAST_EXPECT(nftCount(env, alice) == 0);
429 BEAST_EXPECT(ownerCount(env, alice) == 0);
430 }
431
432 // A lambda verifies that the ledger no longer contains any NFT pages.
433 auto checkNoTokenPages = [this, &env]() {
434 Json::Value jvParams;
435 jvParams[jss::ledger_index] = "current";
436 jvParams[jss::binary] = false;
437 {
438 Json::Value jrr =
439 env.rpc("json", "ledger_data", to_string(jvParams));
440
441 Json::Value& state = jrr[jss::result][jss::state];
442
443 for (Json::UInt i = 0; i < state.size(); ++i)
444 {
445 BEAST_EXPECT(!state[i].isMember(sfNFTokens.jsonName));
446 }
447 }
448 };
449 checkNoTokenPages();
450 {
451 // Generate three packed pages. Then burn the tokens in order from
452 // last to first. This exercises different specific cases where
453 // coalescing pages is not possible.
454 std::vector<uint256> nfts = genPackedTokens();
455 BEAST_EXPECT(nftCount(env, alice) == 96);
456 BEAST_EXPECT(ownerCount(env, alice) == 3);
457
458 // Verify that that all three pages are present and remember the
459 // indexes.
460 auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
461 if (!BEAST_EXPECT(lastNFTokenPage))
462 return;
463
464 uint256 const middleNFTokenPageIndex =
465 lastNFTokenPage->at(sfPreviousPageMin);
466 auto middleNFTokenPage = env.le(keylet::nftpage(
467 keylet::nftpage_min(alice), middleNFTokenPageIndex));
468 if (!BEAST_EXPECT(middleNFTokenPage))
469 return;
470
471 uint256 const firstNFTokenPageIndex =
472 middleNFTokenPage->at(sfPreviousPageMin);
473 auto firstNFTokenPage = env.le(keylet::nftpage(
474 keylet::nftpage_min(alice), firstNFTokenPageIndex));
475 if (!BEAST_EXPECT(firstNFTokenPage))
476 return;
477
478 // Burn almost all the tokens in the very last page.
479 for (int i = 0; i < 31; ++i)
480 {
481 env(token::burn(alice, {nfts.back()}));
482 nfts.pop_back();
483 env.close();
484 }
485
486 // Verify that the last page is still present and contains just one
487 // NFT.
488 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
489 if (!BEAST_EXPECT(lastNFTokenPage))
490 return;
491
492 BEAST_EXPECT(
493 lastNFTokenPage->getFieldArray(sfNFTokens).size() == 1);
494 BEAST_EXPECT(lastNFTokenPage->isFieldPresent(sfPreviousPageMin));
495 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
496
497 // Delete the last token from the last page.
498 env(token::burn(alice, {nfts.back()}));
499 nfts.pop_back();
500 env.close();
501
502 if (features[fixNFTokenPageLinks])
503 {
504 // Removing the last token from the last page deletes the
505 // _previous_ page because we need to preserve that last
506 // page an an anchor. The contents of the next-to-last page
507 // are moved into the last page.
508 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
509 BEAST_EXPECT(lastNFTokenPage);
510 BEAST_EXPECT(
511 lastNFTokenPage->at(~sfPreviousPageMin) ==
512 firstNFTokenPageIndex);
513 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
514 BEAST_EXPECT(
515 lastNFTokenPage->getFieldArray(sfNFTokens).size() == 32);
516
517 // The "middle" page should be gone.
518 middleNFTokenPage = env.le(keylet::nftpage(
519 keylet::nftpage_min(alice), middleNFTokenPageIndex));
520 BEAST_EXPECT(!middleNFTokenPage);
521
522 // The "first" page should still be present and linked to
523 // the last page.
524 firstNFTokenPage = env.le(keylet::nftpage(
525 keylet::nftpage_min(alice), firstNFTokenPageIndex));
526 BEAST_EXPECT(firstNFTokenPage);
527 BEAST_EXPECT(
528 !firstNFTokenPage->isFieldPresent(sfPreviousPageMin));
529 BEAST_EXPECT(
530 firstNFTokenPage->at(~sfNextPageMin) ==
531 lastNFTokenPage->key());
532 BEAST_EXPECT(
533 lastNFTokenPage->getFieldArray(sfNFTokens).size() == 32);
534 }
535 else
536 {
537 // Removing the last token from the last page deletes the last
538 // page. This is a bug. The contents of the next-to-last page
539 // should have been moved into the last page.
540 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
541 BEAST_EXPECT(!lastNFTokenPage);
542
543 // The "middle" page is still present, but has lost the
544 // NextPageMin field.
545 middleNFTokenPage = env.le(keylet::nftpage(
546 keylet::nftpage_min(alice), middleNFTokenPageIndex));
547 if (!BEAST_EXPECT(middleNFTokenPage))
548 return;
549 BEAST_EXPECT(
550 middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
551 BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfNextPageMin));
552 }
553
554 // Delete the rest of the NFTokens.
555 while (!nfts.empty())
556 {
557 env(token::burn(alice, {nfts.back()}));
558 nfts.pop_back();
559 env.close();
560 }
561 BEAST_EXPECT(nftCount(env, alice) == 0);
562 BEAST_EXPECT(ownerCount(env, alice) == 0);
563 }
564 checkNoTokenPages();
565 {
566 // Generate three packed pages. Then burn all tokens in the middle
567 // page. This exercises the case where a page is removed between
568 // two fully populated pages.
569 std::vector<uint256> nfts = genPackedTokens();
570 BEAST_EXPECT(nftCount(env, alice) == 96);
571 BEAST_EXPECT(ownerCount(env, alice) == 3);
572
573 // Verify that that all three pages are present and remember the
574 // indexes.
575 auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
576 if (!BEAST_EXPECT(lastNFTokenPage))
577 return;
578
579 uint256 const middleNFTokenPageIndex =
580 lastNFTokenPage->at(sfPreviousPageMin);
581 auto middleNFTokenPage = env.le(keylet::nftpage(
582 keylet::nftpage_min(alice), middleNFTokenPageIndex));
583 if (!BEAST_EXPECT(middleNFTokenPage))
584 return;
585
586 uint256 const firstNFTokenPageIndex =
587 middleNFTokenPage->at(sfPreviousPageMin);
588 auto firstNFTokenPage = env.le(keylet::nftpage(
589 keylet::nftpage_min(alice), firstNFTokenPageIndex));
590 if (!BEAST_EXPECT(firstNFTokenPage))
591 return;
592
593 for (std::size_t i = 32; i < 64; ++i)
594 {
595 env(token::burn(alice, nfts[i]));
596 env.close();
597 }
598 nfts.erase(nfts.begin() + 32, nfts.begin() + 64);
599 BEAST_EXPECT(nftCount(env, alice) == 64);
600 BEAST_EXPECT(ownerCount(env, alice) == 2);
601
602 // Verify that middle page is gone and the links in the two
603 // remaining pages are correct.
604 middleNFTokenPage = env.le(keylet::nftpage(
605 keylet::nftpage_min(alice), middleNFTokenPageIndex));
606 BEAST_EXPECT(!middleNFTokenPage);
607
608 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
609 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
610 BEAST_EXPECT(
611 lastNFTokenPage->getFieldH256(sfPreviousPageMin) ==
612 firstNFTokenPageIndex);
613
614 firstNFTokenPage = env.le(keylet::nftpage(
615 keylet::nftpage_min(alice), firstNFTokenPageIndex));
616 BEAST_EXPECT(
617 firstNFTokenPage->getFieldH256(sfNextPageMin) ==
618 keylet::nftpage_max(alice).key);
619 BEAST_EXPECT(!firstNFTokenPage->isFieldPresent(sfPreviousPageMin));
620
621 // Burn the remaining nfts.
622 for (uint256 const& nft : nfts)
623 {
624 env(token::burn(alice, {nft}));
625 env.close();
626 }
627 BEAST_EXPECT(nftCount(env, alice) == 0);
628 BEAST_EXPECT(ownerCount(env, alice) == 0);
629 }
630 checkNoTokenPages();
631 {
632 // Generate three packed pages. Then burn all the tokens in the
633 // first page followed by all the tokens in the last page. This
634 // exercises a specific case where coalescing pages is not possible.
635 std::vector<uint256> nfts = genPackedTokens();
636 BEAST_EXPECT(nftCount(env, alice) == 96);
637 BEAST_EXPECT(ownerCount(env, alice) == 3);
638
639 // Verify that that all three pages are present and remember the
640 // indexes.
641 auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
642 if (!BEAST_EXPECT(lastNFTokenPage))
643 return;
644
645 uint256 const middleNFTokenPageIndex =
646 lastNFTokenPage->at(sfPreviousPageMin);
647 auto middleNFTokenPage = env.le(keylet::nftpage(
648 keylet::nftpage_min(alice), middleNFTokenPageIndex));
649 if (!BEAST_EXPECT(middleNFTokenPage))
650 return;
651
652 uint256 const firstNFTokenPageIndex =
653 middleNFTokenPage->at(sfPreviousPageMin);
654 auto firstNFTokenPage = env.le(keylet::nftpage(
655 keylet::nftpage_min(alice), firstNFTokenPageIndex));
656 if (!BEAST_EXPECT(firstNFTokenPage))
657 return;
658
659 // Burn all the tokens in the first page.
660 std::reverse(nfts.begin(), nfts.end());
661 for (int i = 0; i < 32; ++i)
662 {
663 env(token::burn(alice, {nfts.back()}));
664 nfts.pop_back();
665 env.close();
666 }
667
668 // Verify the first page is gone.
669 firstNFTokenPage = env.le(keylet::nftpage(
670 keylet::nftpage_min(alice), firstNFTokenPageIndex));
671 BEAST_EXPECT(!firstNFTokenPage);
672
673 // Check the links in the other two pages.
674 middleNFTokenPage = env.le(keylet::nftpage(
675 keylet::nftpage_min(alice), middleNFTokenPageIndex));
676 if (!BEAST_EXPECT(middleNFTokenPage))
677 return;
678 BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
679 BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfNextPageMin));
680
681 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
682 if (!BEAST_EXPECT(lastNFTokenPage))
683 return;
684 BEAST_EXPECT(lastNFTokenPage->isFieldPresent(sfPreviousPageMin));
685 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
686
687 // Burn all the tokens in the last page.
688 std::reverse(nfts.begin(), nfts.end());
689 for (int i = 0; i < 32; ++i)
690 {
691 env(token::burn(alice, {nfts.back()}));
692 nfts.pop_back();
693 env.close();
694 }
695
696 if (features[fixNFTokenPageLinks])
697 {
698 // Removing the last token from the last page deletes the
699 // _previous_ page because we need to preserve that last
700 // page an an anchor. The contents of the next-to-last page
701 // are moved into the last page.
702 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
703 BEAST_EXPECT(lastNFTokenPage);
704 BEAST_EXPECT(
705 !lastNFTokenPage->isFieldPresent(sfPreviousPageMin));
706 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
707 BEAST_EXPECT(
708 lastNFTokenPage->getFieldArray(sfNFTokens).size() == 32);
709
710 // The "middle" page should be gone.
711 middleNFTokenPage = env.le(keylet::nftpage(
712 keylet::nftpage_min(alice), middleNFTokenPageIndex));
713 BEAST_EXPECT(!middleNFTokenPage);
714
715 // The "first" page should still be gone.
716 firstNFTokenPage = env.le(keylet::nftpage(
717 keylet::nftpage_min(alice), firstNFTokenPageIndex));
718 BEAST_EXPECT(!firstNFTokenPage);
719 }
720 else
721 {
722 // Removing the last token from the last page deletes the last
723 // page. This is a bug. The contents of the next-to-last page
724 // should have been moved into the last page.
725 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
726 BEAST_EXPECT(!lastNFTokenPage);
727
728 // The "middle" page is still present, but has lost the
729 // NextPageMin field.
730 middleNFTokenPage = env.le(keylet::nftpage(
731 keylet::nftpage_min(alice), middleNFTokenPageIndex));
732 if (!BEAST_EXPECT(middleNFTokenPage))
733 return;
734 BEAST_EXPECT(
735 !middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
736 BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfNextPageMin));
737 }
738
739 // Delete the rest of the NFTokens.
740 while (!nfts.empty())
741 {
742 env(token::burn(alice, {nfts.back()}));
743 nfts.pop_back();
744 env.close();
745 }
746 BEAST_EXPECT(nftCount(env, alice) == 0);
747 BEAST_EXPECT(ownerCount(env, alice) == 0);
748 }
749 checkNoTokenPages();
750
751 if (features[fixNFTokenPageLinks])
752 {
753 // Exercise the invariant that the final NFTokenPage of a directory
754 // may not be removed if there are NFTokens in other pages of the
755 // directory.
756 //
757 // We're going to fire an Invariant failure that is difficult to
758 // cause. We do it here because the tools are here.
759 //
760 // See Invariants_test.cpp for examples of other invariant tests
761 // that this one is modeled after.
762
763 // Generate three closely packed NFTokenPages.
764 std::vector<uint256> nfts = genPackedTokens();
765 BEAST_EXPECT(nftCount(env, alice) == 96);
766 BEAST_EXPECT(ownerCount(env, alice) == 3);
767
768 // Burn almost all the tokens in the very last page.
769 for (int i = 0; i < 31; ++i)
770 {
771 env(token::burn(alice, {nfts.back()}));
772 nfts.pop_back();
773 env.close();
774 }
775 {
776 // Create an ApplyContext we can use to run the invariant
777 // checks. These variables must outlive the ApplyContext.
778 OpenView ov{*env.current()};
779 STTx tx{ttACCOUNT_SET, [](STObject&) {}};
781 beast::Journal jlog{sink};
782 ApplyContext ac{
783 env.app(),
784 ov,
785 tx,
787 env.current()->fees().base,
788 tapNONE,
789 jlog};
790
791 // Verify that the last page is present and contains one NFT.
792 auto lastNFTokenPage =
793 ac.view().peek(keylet::nftpage_max(alice));
794 if (!BEAST_EXPECT(lastNFTokenPage))
795 return;
796 BEAST_EXPECT(
797 lastNFTokenPage->getFieldArray(sfNFTokens).size() == 1);
798
799 // Erase that last page.
800 ac.view().erase(lastNFTokenPage);
801
802 // Exercise the invariant.
803 TER terActual = tesSUCCESS;
804 for (TER const& terExpect :
806 {
807 terActual = ac.checkInvariants(terActual, XRPAmount{});
808 BEAST_EXPECT(terExpect == terActual);
809 BEAST_EXPECT(
810 sink.messages().str().starts_with("Invariant failed:"));
811 // uncomment to log the invariant failure message
812 // log << " --> " << sink.messages().str() << std::endl;
813 BEAST_EXPECT(
814 sink.messages().str().find(
815 "Last NFT page deleted with non-empty directory") !=
816 std::string::npos);
817 }
818 }
819 {
820 // Create an ApplyContext we can use to run the invariant
821 // checks. These variables must outlive the ApplyContext.
822 OpenView ov{*env.current()};
823 STTx tx{ttACCOUNT_SET, [](STObject&) {}};
825 beast::Journal jlog{sink};
826 ApplyContext ac{
827 env.app(),
828 ov,
829 tx,
831 env.current()->fees().base,
832 tapNONE,
833 jlog};
834
835 // Verify that the middle page is present.
836 auto lastNFTokenPage =
837 ac.view().peek(keylet::nftpage_max(alice));
838 auto middleNFTokenPage = ac.view().peek(keylet::nftpage(
839 keylet::nftpage_min(alice),
840 lastNFTokenPage->getFieldH256(sfPreviousPageMin)));
841 BEAST_EXPECT(middleNFTokenPage);
842
843 // Remove the NextMinPage link from the middle page to fire
844 // the invariant.
845 middleNFTokenPage->makeFieldAbsent(sfNextPageMin);
846 ac.view().update(middleNFTokenPage);
847
848 // Exercise the invariant.
849 TER terActual = tesSUCCESS;
850 for (TER const& terExpect :
852 {
853 terActual = ac.checkInvariants(terActual, XRPAmount{});
854 BEAST_EXPECT(terExpect == terActual);
855 BEAST_EXPECT(
856 sink.messages().str().starts_with("Invariant failed:"));
857 // uncomment to log the invariant failure message
858 // log << " --> " << sink.messages().str() << std::endl;
859 BEAST_EXPECT(
860 sink.messages().str().find("Lost NextMinPage link") !=
861 std::string::npos);
862 }
863 }
864 }
865 }
866
867 void
869 {
870 // Look at the case where too many offers prevents burning a token.
871 testcase("Burn too many offers");
872
873 using namespace test::jtx;
874
875 // Test that up to 499 buy/sell offers will be removed when NFT is
876 // burned. This is to test that we can successfully remove all offers
877 // if the number of offers is less than 500.
878 {
879 Env env{*this, features};
880
881 Account const alice("alice");
882 Account const becky("becky");
883 env.fund(XRP(100000), alice, becky);
884 env.close();
885
886 // alice creates 498 sell offers and becky creates 1 buy offers.
887 // When the token is burned, 498 sell offers and 1 buy offer are
888 // removed. In total, 499 offers are removed
889 std::vector<uint256> offerIndexes;
890 auto const nftokenID = createNftAndOffers(
891 env, alice, offerIndexes, maxDeletableTokenOfferEntries - 2);
892
893 // Verify all sell offers are present in the ledger.
894 for (uint256 const& offerIndex : offerIndexes)
895 {
896 BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
897 }
898
899 // Becky creates a buy offer
900 uint256 const beckyOfferIndex =
901 keylet::nftoffer(becky, env.seq(becky)).key;
902 env(token::createOffer(becky, nftokenID, drops(1)),
903 token::owner(alice));
904 env.close();
905
906 // Burn the token
907 env(token::burn(alice, nftokenID));
908 env.close();
909
910 // Burning the token should remove all 498 sell offers
911 // that alice created
912 for (uint256 const& offerIndex : offerIndexes)
913 {
914 BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
915 }
916
917 // Burning the token should also remove the one buy offer
918 // that becky created
919 BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex)));
920
921 // alice and becky should have ownerCounts of zero
922 BEAST_EXPECT(ownerCount(env, alice) == 0);
923 BEAST_EXPECT(ownerCount(env, becky) == 0);
924 }
925
926 // Test that up to 500 buy offers are removed when NFT is burned.
927 {
928 Env env{*this, features};
929
930 Account const alice("alice");
931 Account const becky("becky");
932 env.fund(XRP(100000), alice, becky);
933 env.close();
934
935 // alice creates 501 sell offers for the token
936 // After we burn the token, 500 of the sell offers should be
937 // removed, and one is left over
938 std::vector<uint256> offerIndexes;
939 auto const nftokenID = createNftAndOffers(
940 env, alice, offerIndexes, maxDeletableTokenOfferEntries + 1);
941
942 // Verify all sell offers are present in the ledger.
943 for (uint256 const& offerIndex : offerIndexes)
944 {
945 BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
946 }
947
948 // Burn the token
949 env(token::burn(alice, nftokenID));
950 env.close();
951
952 uint32_t offerDeletedCount = 0;
953 // Count the number of sell offers that have been deleted
954 for (uint256 const& offerIndex : offerIndexes)
955 {
956 if (!env.le(keylet::nftoffer(offerIndex)))
957 offerDeletedCount++;
958 }
959
960 BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1);
961
962 // 500 sell offers should be removed
963 BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount);
964
965 // alice should have ownerCounts of one for the orphaned sell offer
966 BEAST_EXPECT(ownerCount(env, alice) == 1);
967 }
968
969 // Test that up to 500 buy/sell offers are removed when NFT is burned.
970 {
971 Env env{*this, features};
972
973 Account const alice("alice");
974 Account const becky("becky");
975 env.fund(XRP(100000), alice, becky);
976 env.close();
977
978 // alice creates 499 sell offers and becky creates 2 buy offers.
979 // When the token is burned, 499 sell offers and 1 buy offer
980 // are removed.
981 // In total, 500 offers are removed
982 std::vector<uint256> offerIndexes;
983 auto const nftokenID = createNftAndOffers(
984 env, alice, offerIndexes, maxDeletableTokenOfferEntries - 1);
985
986 // Verify all sell offers are present in the ledger.
987 for (uint256 const& offerIndex : offerIndexes)
988 {
989 BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
990 }
991
992 // becky creates 2 buy offers
993 env(token::createOffer(becky, nftokenID, drops(1)),
994 token::owner(alice));
995 env.close();
996 env(token::createOffer(becky, nftokenID, drops(1)),
997 token::owner(alice));
998 env.close();
999
1000 // Burn the token
1001 env(token::burn(alice, nftokenID));
1002 env.close();
1003
1004 // Burning the token should remove all 499 sell offers from the
1005 // ledger.
1006 for (uint256 const& offerIndex : offerIndexes)
1007 {
1008 BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
1009 }
1010
1011 // alice should have ownerCount of zero because all her
1012 // sell offers have been deleted
1013 BEAST_EXPECT(ownerCount(env, alice) == 0);
1014
1015 // becky has ownerCount of one due to an orphaned buy offer
1016 BEAST_EXPECT(ownerCount(env, becky) == 1);
1017 }
1018 }
1019
1020 void
1022 {
1023 // Amendment fixNFTokenPageLinks prevents the breakage we want
1024 // to observe.
1025 if (features[fixNFTokenPageLinks])
1026 return;
1027
1028 // a couple of directory merging scenarios that can only be tested by
1029 // inserting and deleting in an ordered fashion. We do that testing
1030 // now.
1031 testcase("Exercise broken links");
1032
1033 using namespace test::jtx;
1034
1035 Account const alice{"alice"};
1036 Account const minter{"minter"};
1037
1038 Env env{*this, features};
1039 env.fund(XRP(1000), alice, minter);
1040
1041 // A lambda that generates 96 nfts packed into three pages of 32 each.
1042 // Returns a sorted vector of the NFTokenIDs packed into the pages.
1043 auto genPackedTokens = [this, &env, &alice, &minter]() {
1045 nfts.reserve(96);
1046
1047 // We want to create fully packed NFT pages. This is a little
1048 // tricky since the system currently in place is inclined to
1049 // assign consecutive tokens to only 16 entries per page.
1050 //
1051 // By manipulating the internal form of the taxon we can force
1052 // creation of NFT pages that are completely full. This lambda
1053 // tells us the taxon value we should pass in in order for the
1054 // internal representation to match the passed in value.
1055 auto internalTaxon = [&env](
1056 Account const& acct,
1057 std::uint32_t taxon) -> std::uint32_t {
1058 std::uint32_t tokenSeq =
1059 env.le(acct)->at(~sfMintedNFTokens).value_or(0);
1060
1061 // We must add FirstNFTokenSequence.
1062 tokenSeq += env.le(acct)
1063 ->at(~sfFirstNFTokenSequence)
1064 .value_or(env.seq(acct));
1065
1066 return toUInt32(
1067 nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
1068 };
1069
1070 for (std::uint32_t i = 0; i < 96; ++i)
1071 {
1072 // In order to fill the pages we use the taxon to break them
1073 // into groups of 16 entries. By having the internal
1074 // representation of the taxon go...
1075 // 0, 3, 2, 5, 4, 7...
1076 // in sets of 16 NFTs we can get each page to be fully
1077 // populated.
1078 std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
1079 uint32_t const extTaxon = internalTaxon(minter, intTaxon);
1080 nfts.push_back(
1081 token::getNextID(env, minter, extTaxon, tfTransferable));
1082 env(token::mint(minter, extTaxon), txflags(tfTransferable));
1083 env.close();
1084
1085 // Minter creates an offer for the NFToken.
1086 uint256 const minterOfferIndex =
1087 keylet::nftoffer(minter, env.seq(minter)).key;
1088 env(token::createOffer(minter, nfts.back(), XRP(0)),
1089 txflags(tfSellNFToken));
1090 env.close();
1091
1092 // alice accepts the offer.
1093 env(token::acceptSellOffer(alice, minterOfferIndex));
1094 env.close();
1095 }
1096
1097 // Sort the NFTs so they are listed in storage order, not
1098 // creation order.
1099 std::sort(nfts.begin(), nfts.end());
1100
1101 // Verify that the ledger does indeed contain exactly three pages
1102 // of NFTs with 32 entries in each page.
1103 Json::Value jvParams;
1104 jvParams[jss::ledger_index] = "current";
1105 jvParams[jss::binary] = false;
1106 {
1107 Json::Value jrr =
1108 env.rpc("json", "ledger_data", to_string(jvParams));
1109
1110 Json::Value& state = jrr[jss::result][jss::state];
1111
1112 int pageCount = 0;
1113 for (Json::UInt i = 0; i < state.size(); ++i)
1114 {
1115 if (state[i].isMember(sfNFTokens.jsonName) &&
1116 state[i][sfNFTokens.jsonName].isArray())
1117 {
1118 BEAST_EXPECT(
1119 state[i][sfNFTokens.jsonName].size() == 32);
1120 ++pageCount;
1121 }
1122 }
1123 // If this check fails then the internal NFT directory logic
1124 // has changed.
1125 BEAST_EXPECT(pageCount == 3);
1126 }
1127 return nfts;
1128 };
1129
1130 // Generate three packed pages.
1131 std::vector<uint256> nfts = genPackedTokens();
1132 BEAST_EXPECT(nftCount(env, alice) == 96);
1133 BEAST_EXPECT(ownerCount(env, alice) == 3);
1134
1135 // Verify that that all three pages are present and remember the
1136 // indexes.
1137 auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
1138 if (!BEAST_EXPECT(lastNFTokenPage))
1139 return;
1140
1141 uint256 const middleNFTokenPageIndex =
1142 lastNFTokenPage->at(sfPreviousPageMin);
1143 auto middleNFTokenPage = env.le(keylet::nftpage(
1144 keylet::nftpage_min(alice), middleNFTokenPageIndex));
1145 if (!BEAST_EXPECT(middleNFTokenPage))
1146 return;
1147
1148 uint256 const firstNFTokenPageIndex =
1149 middleNFTokenPage->at(sfPreviousPageMin);
1150 auto firstNFTokenPage = env.le(
1151 keylet::nftpage(keylet::nftpage_min(alice), firstNFTokenPageIndex));
1152 if (!BEAST_EXPECT(firstNFTokenPage))
1153 return;
1154
1155 // Sell all the tokens in the very last page back to minter.
1156 std::vector<uint256> last32NFTs;
1157 for (int i = 0; i < 32; ++i)
1158 {
1159 last32NFTs.push_back(nfts.back());
1160 nfts.pop_back();
1161
1162 // alice creates an offer for the NFToken.
1163 uint256 const aliceOfferIndex =
1164 keylet::nftoffer(alice, env.seq(alice)).key;
1165 env(token::createOffer(alice, last32NFTs.back(), XRP(0)),
1166 txflags(tfSellNFToken));
1167 env.close();
1168
1169 // minter accepts the offer.
1170 env(token::acceptSellOffer(minter, aliceOfferIndex));
1171 env.close();
1172 }
1173
1174 // Removing the last token from the last page deletes alice's last
1175 // page. This is a bug. The contents of the next-to-last page
1176 // should have been moved into the last page.
1177 lastNFTokenPage = env.le(keylet::nftpage_max(alice));
1178 BEAST_EXPECT(!lastNFTokenPage);
1179 BEAST_EXPECT(ownerCount(env, alice) == 2);
1180
1181 // The "middle" page is still present, but has lost the
1182 // NextPageMin field.
1183 middleNFTokenPage = env.le(keylet::nftpage(
1184 keylet::nftpage_min(alice), middleNFTokenPageIndex));
1185 if (!BEAST_EXPECT(middleNFTokenPage))
1186 return;
1187 BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
1188 BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfNextPageMin));
1189
1190 // Attempt to delete alice's account, but fail because she owns NFTs.
1191 auto const acctDelFee{drops(env.current()->fees().increment)};
1192 env(acctdelete(alice, minter),
1193 fee(acctDelFee),
1194 ter(tecHAS_OBLIGATIONS));
1195 env.close();
1196
1197 // minter sells the last 32 NFTs back to alice.
1198 for (uint256 nftID : last32NFTs)
1199 {
1200 // minter creates an offer for the NFToken.
1201 uint256 const minterOfferIndex =
1202 keylet::nftoffer(minter, env.seq(minter)).key;
1203 env(token::createOffer(minter, nftID, XRP(0)),
1204 txflags(tfSellNFToken));
1205 env.close();
1206
1207 // alice accepts the offer.
1208 env(token::acceptSellOffer(alice, minterOfferIndex));
1209 env.close();
1210 }
1211 BEAST_EXPECT(ownerCount(env, alice) == 3); // Three NFTokenPages.
1212
1213 // alice has an NFToken directory with a broken link in the middle.
1214 {
1215 // Try the account_objects RPC command. Alice's account only shows
1216 // two NFT pages even though she owns more.
1217 Json::Value acctObjs = [&env, &alice]() {
1218 Json::Value params;
1219 params[jss::account] = alice.human();
1220 return env.rpc("json", "account_objects", to_string(params));
1221 }();
1222 BEAST_EXPECT(!acctObjs.isMember(jss::marker));
1223 BEAST_EXPECT(
1224 acctObjs[jss::result][jss::account_objects].size() == 2);
1225 }
1226 {
1227 // Try the account_nfts RPC command. It only returns 64 NFTs
1228 // although alice owns 96.
1229 Json::Value aliceNFTs = [&env, &alice]() {
1230 Json::Value params;
1231 params[jss::account] = alice.human();
1232 params[jss::type] = "state";
1233 return env.rpc("json", "account_nfts", to_string(params));
1234 }();
1235 BEAST_EXPECT(!aliceNFTs.isMember(jss::marker));
1236 BEAST_EXPECT(
1237 aliceNFTs[jss::result][jss::account_nfts].size() == 64);
1238 }
1239 }
1240
1241protected:
1243
1244 void
1246 {
1247 testBurnRandom(features);
1248 testBurnSequential(features);
1249 testBurnTooManyOffers(features);
1250 exerciseBrokenLinks(features);
1251 }
1252
1253public:
1254 void
1255 run() override
1256 {
1257 testWithFeats(allFeatures - fixNFTokenPageLinks);
1259 }
1260};
1261
1262BEAST_DEFINE_TESTSUITE_PRIO(NFTokenBurn, app, ripple, 3);
1263
1264} // namespace ripple
T back(T... args)
T begin(T... args)
Represents a JSON value.
Definition json_value.h:130
bool isArray() const
UInt size() const
Number of values in array or object.
std::string toStyledString() const
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
A generic endpoint for log messages.
Definition Journal.h:41
A testsuite class.
Definition suite.h:52
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:152
State information when applying a tx.
Application & app
void run() override
Runs the suite.
void exerciseBrokenLinks(FeatureBitset features)
void testBurnTooManyOffers(FeatureBitset features)
static std::uint32_t nftCount(test::jtx::Env &env, test::jtx::Account const &acct)
void printNFTPages(test::jtx::Env &env, Volume vol)
void testBurnSequential(FeatureBitset features)
void testBurnRandom(FeatureBitset features)
uint256 createNftAndOffers(test::jtx::Env &env, test::jtx::Account const &owner, std::vector< uint256 > &offerIndexes, size_t const tokenCancelCount)
void testWithFeats(FeatureBitset features)
FeatureBitset const allFeatures
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:46
Immutable cryptographic account descriptor.
Definition Account.h:20
std::string const & human() const
Returns the human readable public key.
Definition Account.h:99
A transaction testing environment.
Definition Env.h:102
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:250
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:103
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:772
T empty(T... args)
T end(T... args)
T endl(T... args)
T erase(T... args)
unsigned int UInt
Keylet nftpage(Keylet const &k, uint256 const &token)
Definition Indexes.cpp:400
Keylet nftpage_min(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:384
Keylet nftpage_max(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:392
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:408
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition nft.h:65
Taxon toTaxon(std::uint32_t i)
Definition nft.h:23
FeatureBitset testable_amendments()
Definition Env.h:55
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:211
std::size_t constexpr maxTokenOfferCancelCount
The maximum number of token offers that can be canceled at once.
Definition Protocol.h:52
std::uint16_t constexpr maxTransferFee
The maximum token transfer fee allowed.
Definition Protocol.h:66
std::size_t constexpr maxDeletableTokenOfferEntries
The maximum number of offers in an offer directory for NFT to be burnable.
Definition Protocol.h:55
constexpr std::uint32_t const tfBurnable
Definition TxFlags.h:120
@ tefINVARIANT_FAILED
Definition TER.h:164
std::size_t constexpr maxTokenURILength
The maximum length of a URI inside an NFT.
Definition Protocol.h:69
@ tecHAS_OBLIGATIONS
Definition TER.h:299
@ tecINVARIANT_FAILED
Definition TER.h:295
@ tesSUCCESS
Definition TER.h:226
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
@ tapNONE
Definition ApplyView.h:12
TERSubset< CanCvtToTER > TER
Definition TER.h:630
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:123
T pop_back(T... args)
T push_back(T... args)
T reserve(T... args)
T reverse(T... args)
T size(T... args)
T sort(T... args)
uint256 key
Definition Keylet.h:21