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