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