rippled
NFTokenBurn_test.cpp
1 //------------------------------------------------------------------------------
2 /*
3  This file is part of rippled: https://github.com/ripple/rippled
4  Copyright (c) 2021 Ripple Labs Inc.
5 
6  Permission to use, copy, modify, and/or distribute this software for any
7  purpose with or without fee is hereby granted, provided that the above
8  copyright notice and this permission notice appear in all copies.
9 
10  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18 //==============================================================================
19 
20 #include <ripple/app/tx/impl/details/NFTokenUtils.h>
21 #include <ripple/protocol/Feature.h>
22 #include <ripple/protocol/jss.h>
23 #include <test/jtx.h>
24 
25 #include <random>
26 
27 namespace ripple {
28 
29 class NFTokenBurn_test : public beast::unit_test::suite
30 {
31  // Helper function that returns the owner count of an account root.
32  static std::uint32_t
33  ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
34  {
35  std::uint32_t ret{0};
36  if (auto const sleAcct = env.le(acct))
37  ret = sleAcct->at(sfOwnerCount);
38  return ret;
39  }
40 
41  // Helper function that returns the number of nfts owned by an account.
42  static std::uint32_t
44  {
45  Json::Value params;
46  params[jss::account] = acct.human();
47  params[jss::type] = "state";
48  Json::Value nfts = env.rpc("json", "account_nfts", to_string(params));
49  return nfts[jss::result][jss::account_nfts].size();
50  };
51 
52  // Helper function that returns new nft id for an account and create
53  // specified number of sell offers
54  uint256
56  test::jtx::Env& env,
57  test::jtx::Account const& owner,
58  std::vector<uint256>& offerIndexes,
59  size_t const tokenCancelCount)
60  {
61  using namespace test::jtx;
62  uint256 const nftokenID =
63  token::getNextID(env, owner, 0, tfTransferable);
64  env(token::mint(owner, 0),
65  token::uri(std::string(maxTokenURILength, 'u')),
66  txflags(tfTransferable));
67  env.close();
68 
69  offerIndexes.reserve(tokenCancelCount);
70 
71  for (uint32_t i = 0; i < tokenCancelCount; ++i)
72  {
73  // Create sell offer
74  offerIndexes.push_back(keylet::nftoffer(owner, env.seq(owner)).key);
75  env(token::createOffer(owner, nftokenID, drops(1)),
76  txflags(tfSellNFToken));
77  env.close();
78  }
79 
80  return nftokenID;
81  };
82 
83  void
85  {
86  // Exercise a number of conditions with NFT burning.
87  testcase("Burn random");
88 
89  using namespace test::jtx;
90 
91  Env env{*this, features};
92 
93  // Keep information associated with each account together.
94  struct AcctStat
95  {
96  test::jtx::Account const acct;
98 
99  AcctStat(char const* name) : acct(name)
100  {
101  }
102 
103  operator test::jtx::Account() const
104  {
105  return acct;
106  }
107  };
108  AcctStat alice{"alice"};
109  AcctStat becky{"becky"};
110  AcctStat minter{"minter"};
111 
112  env.fund(XRP(10000), alice, becky, minter);
113  env.close();
114 
115  // Both alice and minter mint nfts in case that makes any difference.
116  env(token::setMinter(alice, minter));
117  env.close();
118 
119  // Create enough NFTs that alice, becky, and minter can all have
120  // at least three pages of NFTs. This will cause more activity in
121  // the page coalescing code. If we make 210 NFTs in total, we can
122  // have alice and minter each make 105. That will allow us to
123  // distribute 70 NFTs to our three participants.
124  //
125  // Give each NFT a pseudo-randomly chosen fee so the NFTs are
126  // distributed pseudo-randomly through the pages. This should
127  // prevent alice's and minter's NFTs from clustering together
128  // in becky's directory.
129  //
130  // Use a default initialized mercenne_twister because we want the
131  // effect of random numbers, but we want the test to run the same
132  // way each time.
133  std::mt19937 engine;
135  decltype(maxTransferFee){}, maxTransferFee);
136 
137  alice.nfts.reserve(105);
138  while (alice.nfts.size() < 105)
139  {
140  std::uint16_t const xferFee = feeDist(engine);
141  alice.nfts.push_back(token::getNextID(
142  env, alice, 0u, tfTransferable | tfBurnable, xferFee));
143  env(token::mint(alice),
144  txflags(tfTransferable | tfBurnable),
145  token::xferFee(xferFee));
146  env.close();
147  }
148 
149  minter.nfts.reserve(105);
150  while (minter.nfts.size() < 105)
151  {
152  std::uint16_t const xferFee = feeDist(engine);
153  minter.nfts.push_back(token::getNextID(
154  env, alice, 0u, tfTransferable | tfBurnable, xferFee));
155  env(token::mint(minter),
156  txflags(tfTransferable | tfBurnable),
157  token::xferFee(xferFee),
158  token::issuer(alice));
159  env.close();
160  }
161 
162  // All of the NFTs are now minted. Transfer 35 each over to becky so
163  // we end up with 70 NFTs in each account.
164  becky.nfts.reserve(70);
165  {
166  auto aliceIter = alice.nfts.begin();
167  auto minterIter = minter.nfts.begin();
168  while (becky.nfts.size() < 70)
169  {
170  // We do the same work on alice and minter, so make a lambda.
171  auto xferNFT = [&env, &becky](AcctStat& acct, auto& iter) {
172  uint256 offerIndex =
173  keylet::nftoffer(acct.acct, env.seq(acct.acct)).key;
174  env(token::createOffer(acct, *iter, XRP(0)),
175  txflags(tfSellNFToken));
176  env.close();
177  env(token::acceptSellOffer(becky, offerIndex));
178  env.close();
179  becky.nfts.push_back(*iter);
180  iter = acct.nfts.erase(iter);
181  iter += 2;
182  };
183  xferNFT(alice, aliceIter);
184  xferNFT(minter, minterIter);
185  }
186  BEAST_EXPECT(aliceIter == alice.nfts.end());
187  BEAST_EXPECT(minterIter == minter.nfts.end());
188  }
189 
190  // Now all three participants have 70 NFTs.
191  BEAST_EXPECT(nftCount(env, alice.acct) == 70);
192  BEAST_EXPECT(nftCount(env, becky.acct) == 70);
193  BEAST_EXPECT(nftCount(env, minter.acct) == 70);
194 
195  // Next we'll create offers for all of those NFTs. This calls for
196  // another lambda.
197  auto addOffers =
198  [&env](AcctStat& owner, AcctStat& other1, AcctStat& other2) {
199  for (uint256 nft : owner.nfts)
200  {
201  // Create sell offers for owner.
202  env(token::createOffer(owner, nft, drops(1)),
203  txflags(tfSellNFToken),
204  token::destination(other1));
205  env(token::createOffer(owner, nft, drops(1)),
206  txflags(tfSellNFToken),
207  token::destination(other2));
208  env.close();
209 
210  // Create buy offers for other1 and other2.
211  env(token::createOffer(other1, nft, drops(1)),
212  token::owner(owner));
213  env(token::createOffer(other2, nft, drops(1)),
214  token::owner(owner));
215  env.close();
216 
217  env(token::createOffer(other2, nft, drops(2)),
218  token::owner(owner));
219  env(token::createOffer(other1, nft, drops(2)),
220  token::owner(owner));
221  env.close();
222  }
223  };
224  addOffers(alice, becky, minter);
225  addOffers(becky, minter, alice);
226  addOffers(minter, alice, becky);
227  BEAST_EXPECT(ownerCount(env, alice) == 424);
228  BEAST_EXPECT(ownerCount(env, becky) == 424);
229  BEAST_EXPECT(ownerCount(env, minter) == 424);
230 
231  // Now each of the 270 NFTs has six offers associated with it.
232  // Randomly select an NFT out of the pile and burn it. Continue
233  // the process until all NFTs are burned.
234  AcctStat* const stats[3] = {&alice, &becky, &minter};
237 
238  while (stats[0]->nfts.size() > 0 || stats[1]->nfts.size() > 0 ||
239  stats[2]->nfts.size() > 0)
240  {
241  // Pick an account to burn an nft. If there are no nfts left
242  // pick again.
243  AcctStat& owner = *(stats[acctDist(engine)]);
244  if (owner.nfts.empty())
245  continue;
246 
247  // Pick one of the nfts.
249  0lu, owner.nfts.size() - 1);
250  auto nftIter = owner.nfts.begin() + nftDist(engine);
251  uint256 const nft = *nftIter;
252  owner.nfts.erase(nftIter);
253 
254  // Decide which of the accounts should burn the nft. If the
255  // owner is becky then any of the three accounts can burn.
256  // Otherwise either alice or minter can burn.
257  AcctStat& burner = owner.acct == becky.acct
258  ? *(stats[acctDist(engine)])
259  : mintDist(engine) ? alice : minter;
260 
261  if (owner.acct == burner.acct)
262  env(token::burn(burner, nft));
263  else
264  env(token::burn(burner, nft), token::owner(owner));
265  env.close();
266 
267  // Every time we burn an nft, the number of nfts they hold should
268  // match the number of nfts we think they hold.
269  BEAST_EXPECT(nftCount(env, alice.acct) == alice.nfts.size());
270  BEAST_EXPECT(nftCount(env, becky.acct) == becky.nfts.size());
271  BEAST_EXPECT(nftCount(env, minter.acct) == minter.nfts.size());
272  }
273  BEAST_EXPECT(nftCount(env, alice.acct) == 0);
274  BEAST_EXPECT(nftCount(env, becky.acct) == 0);
275  BEAST_EXPECT(nftCount(env, minter.acct) == 0);
276 
277  // When all nfts are burned none of the accounts should have
278  // an ownerCount.
279  BEAST_EXPECT(ownerCount(env, alice) == 0);
280  BEAST_EXPECT(ownerCount(env, becky) == 0);
281  BEAST_EXPECT(ownerCount(env, minter) == 0);
282  }
283 
284  void
286  {
287  // The earlier burn test randomizes which nft is burned. There are
288  // a couple of directory merging scenarios that can only be tested by
289  // inserting and deleting in an ordered fashion. We do that testing
290  // now.
291  testcase("Burn sequential");
292 
293  using namespace test::jtx;
294 
295  Account const alice{"alice"};
296 
297  Env env{*this, features};
298  env.fund(XRP(1000), alice);
299 
300  // printNFTPages is a lambda that may be used for debugging.
301  //
302  // It uses the ledger RPC command to show the NFT pages in the ledger.
303  // This parameter controls how noisy the output is.
304  enum Volume : bool {
305  quiet = false,
306  noisy = true,
307  };
308 
309  [[maybe_unused]] auto printNFTPages = [&env](Volume vol) {
310  Json::Value jvParams;
311  jvParams[jss::ledger_index] = "current";
312  jvParams[jss::binary] = false;
313  {
314  Json::Value jrr = env.rpc(
315  "json",
316  "ledger_data",
317  boost::lexical_cast<std::string>(jvParams));
318 
319  // Iterate the state and print all NFTokenPages.
320  if (!jrr.isMember(jss::result) ||
321  !jrr[jss::result].isMember(jss::state))
322  {
323  std::cout << "No ledger state found!" << std::endl;
324  return;
325  }
326  Json::Value& state = jrr[jss::result][jss::state];
327  if (!state.isArray())
328  {
329  std::cout << "Ledger state is not array!" << std::endl;
330  return;
331  }
332  for (Json::UInt i = 0; i < state.size(); ++i)
333  {
334  if (state[i].isMember(sfNFTokens.jsonName) &&
335  state[i][sfNFTokens.jsonName].isArray())
336  {
337  std::uint32_t tokenCount =
338  state[i][sfNFTokens.jsonName].size();
339  std::cout << tokenCount << " NFTokens in page "
340  << state[i][jss::index].asString()
341  << std::endl;
342 
343  if (vol == noisy)
344  {
345  std::cout << state[i].toStyledString() << std::endl;
346  }
347  else
348  {
349  if (tokenCount > 0)
350  std::cout << "first: "
351  << state[i][sfNFTokens.jsonName][0u]
352  .toStyledString()
353  << std::endl;
354  if (tokenCount > 1)
355  std::cout << "last: "
356  << state[i][sfNFTokens.jsonName]
357  [tokenCount - 1]
358  .toStyledString()
359  << std::endl;
360  }
361  }
362  }
363  }
364  };
365 
366  // A lambda that generates 96 nfts packed into three pages of 32 each.
367  auto genPackedTokens = [this, &env, &alice](
368  std::vector<uint256>& nfts) {
369  nfts.clear();
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](
381  Account const& acct,
382  std::uint32_t taxon) -> std::uint32_t {
383  std::uint32_t const tokenSeq = {
384  env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
385  return toUInt32(
386  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::sort(nfts.begin(), nfts.end());
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(
415  "json",
416  "ledger_data",
417  boost::lexical_cast<std::string>(jvParams));
418 
419  Json::Value& state = jrr[jss::result][jss::state];
420 
421  int pageCount = 0;
422  for (Json::UInt i = 0; i < state.size(); ++i)
423  {
424  if (state[i].isMember(sfNFTokens.jsonName) &&
425  state[i][sfNFTokens.jsonName].isArray())
426  {
427  BEAST_EXPECT(
428  state[i][sfNFTokens.jsonName].size() == 32);
429  ++pageCount;
430  }
431  }
432  // If this check fails then the internal NFT directory logic
433  // has changed.
434  BEAST_EXPECT(pageCount == 3);
435  }
436  };
437 
438  // Generate three packed pages. Then burn the tokens in order from
439  // first to last. This exercises specific cases where coalescing
440  // pages is not possible.
442  genPackedTokens(nfts);
443  BEAST_EXPECT(nftCount(env, alice) == 96);
444  BEAST_EXPECT(ownerCount(env, alice) == 3);
445 
446  for (uint256 const& nft : nfts)
447  {
448  env(token::burn(alice, {nft}));
449  env.close();
450  }
451  BEAST_EXPECT(nftCount(env, alice) == 0);
452  BEAST_EXPECT(ownerCount(env, alice) == 0);
453 
454  // A lambda verifies that the ledger no longer contains any NFT pages.
455  auto checkNoTokenPages = [this, &env]() {
456  Json::Value jvParams;
457  jvParams[jss::ledger_index] = "current";
458  jvParams[jss::binary] = false;
459  {
460  Json::Value jrr = env.rpc(
461  "json",
462  "ledger_data",
463  boost::lexical_cast<std::string>(jvParams));
464 
465  Json::Value& state = jrr[jss::result][jss::state];
466 
467  for (Json::UInt i = 0; i < state.size(); ++i)
468  {
469  BEAST_EXPECT(!state[i].isMember(sfNFTokens.jsonName));
470  }
471  }
472  };
473  checkNoTokenPages();
474 
475  // Generate three packed pages. Then burn the tokens in order from
476  // last to first. This exercises different specific cases where
477  // coalescing pages is not possible.
478  genPackedTokens(nfts);
479  BEAST_EXPECT(nftCount(env, alice) == 96);
480  BEAST_EXPECT(ownerCount(env, alice) == 3);
481 
482  std::reverse(nfts.begin(), nfts.end());
483  for (uint256 const& nft : nfts)
484  {
485  env(token::burn(alice, {nft}));
486  env.close();
487  }
488  BEAST_EXPECT(nftCount(env, alice) == 0);
489  BEAST_EXPECT(ownerCount(env, alice) == 0);
490  checkNoTokenPages();
491 
492  // Generate three packed pages. Then burn all tokens in the middle
493  // page. This exercises the case where a page is removed between
494  // two fully populated pages.
495  genPackedTokens(nfts);
496  BEAST_EXPECT(nftCount(env, alice) == 96);
497  BEAST_EXPECT(ownerCount(env, alice) == 3);
498 
499  for (std::size_t i = 32; i < 64; ++i)
500  {
501  env(token::burn(alice, nfts[i]));
502  env.close();
503  }
504  nfts.erase(nfts.begin() + 32, nfts.begin() + 64);
505  BEAST_EXPECT(nftCount(env, alice) == 64);
506  BEAST_EXPECT(ownerCount(env, alice) == 2);
507 
508  // Burn the remaining nfts.
509  for (uint256 const& nft : nfts)
510  {
511  env(token::burn(alice, {nft}));
512  env.close();
513  }
514  BEAST_EXPECT(nftCount(env, alice) == 0);
515  checkNoTokenPages();
516  }
517 
518  void
520  {
521  // Look at the case where too many offers prevents burning a token.
522  testcase("Burn too many offers");
523 
524  using namespace test::jtx;
525 
526  // Test what happens if a NFT is unburnable when there are
527  // more than 500 offers, before fixNonFungibleTokensV1_2 goes live
528  if (!features[fixNonFungibleTokensV1_2])
529  {
530  Env env{*this, features};
531 
532  Account const alice("alice");
533  Account const becky("becky");
534  env.fund(XRP(1000), alice, becky);
535  env.close();
536 
537  // We structure the test to try and maximize the metadata produced.
538  // This verifies that we don't create too much metadata during a
539  // maximal burn operation.
540  //
541  // 1. alice mints an nft with a full-sized URI.
542  // 2. We create 500 new accounts, each of which creates an offer
543  // for alice's nft.
544  // 3. becky creates one more offer for alice's NFT
545  // 4. Attempt to burn the nft which fails because there are too
546  // many offers.
547  // 5. Cancel becky's offer and the nft should become burnable.
548  uint256 const nftokenID =
549  token::getNextID(env, alice, 0, tfTransferable);
550  env(token::mint(alice, 0),
551  token::uri(std::string(maxTokenURILength, 'u')),
552  txflags(tfTransferable));
553  env.close();
554 
555  std::vector<uint256> offerIndexes;
556  offerIndexes.reserve(maxTokenOfferCancelCount);
557  for (std::uint32_t i = 0; i < maxTokenOfferCancelCount; ++i)
558  {
559  Account const acct(std::string("acct") + std::to_string(i));
560  env.fund(XRP(1000), acct);
561  env.close();
562 
563  offerIndexes.push_back(
564  keylet::nftoffer(acct, env.seq(acct)).key);
565  env(token::createOffer(acct, nftokenID, drops(1)),
566  token::owner(alice));
567  env.close();
568  }
569 
570  // Verify all offers are present in the ledger.
571  for (uint256 const& offerIndex : offerIndexes)
572  {
573  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
574  }
575 
576  // Create one too many offers.
577  uint256 const beckyOfferIndex =
578  keylet::nftoffer(becky, env.seq(becky)).key;
579  env(token::createOffer(becky, nftokenID, drops(1)),
580  token::owner(alice));
581 
582  // Attempt to burn the nft which should fail.
583  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
584 
585  // Close enough ledgers that the burn transaction is no longer
586  // retried.
587  for (int i = 0; i < 10; ++i)
588  env.close();
589 
590  // Cancel becky's offer, but alice adds a sell offer. The token
591  // should still not be burnable.
592  env(token::cancelOffer(becky, {beckyOfferIndex}));
593  env.close();
594 
595  uint256 const aliceOfferIndex =
596  keylet::nftoffer(alice, env.seq(alice)).key;
597  env(token::createOffer(alice, nftokenID, drops(1)),
598  txflags(tfSellNFToken));
599  env.close();
600 
601  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
602  env.close();
603 
604  // Cancel alice's sell offer. Now the token should be burnable.
605  env(token::cancelOffer(alice, {aliceOfferIndex}));
606  env.close();
607 
608  env(token::burn(alice, nftokenID));
609  env.close();
610 
611  // Burning the token should remove all the offers from the ledger.
612  for (uint256 const& offerIndex : offerIndexes)
613  {
614  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
615  }
616 
617  // Both alice and becky should have ownerCounts of zero.
618  BEAST_EXPECT(ownerCount(env, alice) == 0);
619  BEAST_EXPECT(ownerCount(env, becky) == 0);
620  }
621 
622  // Test that up to 499 buy/sell offers will be removed when NFT is
623  // burned after fixNonFungibleTokensV1_2 is enabled. This is to test
624  // that we can successfully remove all offers if the number of offers is
625  // less than 500.
626  if (features[fixNonFungibleTokensV1_2])
627  {
628  Env env{*this, features};
629 
630  Account const alice("alice");
631  Account const becky("becky");
632  env.fund(XRP(100000), alice, becky);
633  env.close();
634 
635  // alice creates 498 sell offers and becky creates 1 buy offers.
636  // When the token is burned, 498 sell offers and 1 buy offer are
637  // removed. In total, 499 offers are removed
638  std::vector<uint256> offerIndexes;
639  auto const nftokenID = createNftAndOffers(
640  env, alice, offerIndexes, maxDeletableTokenOfferEntries - 2);
641 
642  // Verify all sell offers are present in the ledger.
643  for (uint256 const& offerIndex : offerIndexes)
644  {
645  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
646  }
647 
648  // Becky creates a buy offer
649  uint256 const beckyOfferIndex =
650  keylet::nftoffer(becky, env.seq(becky)).key;
651  env(token::createOffer(becky, nftokenID, drops(1)),
652  token::owner(alice));
653  env.close();
654 
655  // Burn the token
656  env(token::burn(alice, nftokenID));
657  env.close();
658 
659  // Burning the token should remove all 498 sell offers
660  // that alice created
661  for (uint256 const& offerIndex : offerIndexes)
662  {
663  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
664  }
665 
666  // Burning the token should also remove the one buy offer
667  // that becky created
668  BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex)));
669 
670  // alice and becky should have ownerCounts of zero
671  BEAST_EXPECT(ownerCount(env, alice) == 0);
672  BEAST_EXPECT(ownerCount(env, becky) == 0);
673  }
674 
675  // Test that up to 500 buy offers are removed when NFT is burned
676  // after fixNonFungibleTokensV1_2 is enabled
677  if (features[fixNonFungibleTokensV1_2])
678  {
679  Env env{*this, features};
680 
681  Account const alice("alice");
682  Account const becky("becky");
683  env.fund(XRP(100000), alice, becky);
684  env.close();
685 
686  // alice creates 501 sell offers for the token
687  // After we burn the token, 500 of the sell offers should be
688  // removed, and one is left over
689  std::vector<uint256> offerIndexes;
690  auto const nftokenID = createNftAndOffers(
691  env, alice, offerIndexes, maxDeletableTokenOfferEntries + 1);
692 
693  // Verify all sell offers are present in the ledger.
694  for (uint256 const& offerIndex : offerIndexes)
695  {
696  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
697  }
698 
699  // Burn the token
700  env(token::burn(alice, nftokenID));
701  env.close();
702 
703  uint32_t offerDeletedCount = 0;
704  // Count the number of sell offers that have been deleted
705  for (uint256 const& offerIndex : offerIndexes)
706  {
707  if (!env.le(keylet::nftoffer(offerIndex)))
708  offerDeletedCount++;
709  }
710 
711  BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1);
712 
713  // 500 sell offers should be removed
714  BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount);
715 
716  // alice should have ownerCounts of one for the orphaned sell offer
717  BEAST_EXPECT(ownerCount(env, alice) == 1);
718  }
719 
720  // Test that up to 500 buy/sell offers are removed when NFT is burned
721  // after fixNonFungibleTokensV1_2 is enabled
722  if (features[fixNonFungibleTokensV1_2])
723  {
724  Env env{*this, features};
725 
726  Account const alice("alice");
727  Account const becky("becky");
728  env.fund(XRP(100000), alice, becky);
729  env.close();
730 
731  // alice creates 499 sell offers and becky creates 2 buy offers.
732  // When the token is burned, 499 sell offers and 1 buy offer
733  // are removed.
734  // In total, 500 offers are removed
735  std::vector<uint256> offerIndexes;
736  auto const nftokenID = createNftAndOffers(
737  env, alice, offerIndexes, maxDeletableTokenOfferEntries - 1);
738 
739  // Verify all sell offers are present in the ledger.
740  for (uint256 const& offerIndex : offerIndexes)
741  {
742  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
743  }
744 
745  // becky creates 2 buy offers
746  env(token::createOffer(becky, nftokenID, drops(1)),
747  token::owner(alice));
748  env.close();
749  env(token::createOffer(becky, nftokenID, drops(1)),
750  token::owner(alice));
751  env.close();
752 
753  // Burn the token
754  env(token::burn(alice, nftokenID));
755  env.close();
756 
757  // Burning the token should remove all 499 sell offers from the
758  // ledger.
759  for (uint256 const& offerIndex : offerIndexes)
760  {
761  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
762  }
763 
764  // alice should have ownerCount of zero because all her
765  // sell offers have been deleted
766  BEAST_EXPECT(ownerCount(env, alice) == 0);
767 
768  // becky has ownerCount of one due to an orphaned buy offer
769  BEAST_EXPECT(ownerCount(env, becky) == 1);
770  }
771  }
772 
773  void
775  {
776  testBurnRandom(features);
777  testBurnSequential(features);
778  testBurnTooManyOffers(features);
779  }
780 
781 public:
782  void
783  run() override
784  {
785  using namespace test::jtx;
786  FeatureBitset const all{supported_amendments()};
787  FeatureBitset const fixNFTDir{fixNFTokenDirV1};
788 
792  }
793 };
794 
795 BEAST_DEFINE_TESTSUITE_PRIO(NFTokenBurn, tx, ripple, 3);
796 
797 } // namespace ripple
ripple::maxTransferFee
constexpr std::uint16_t maxTransferFee
The maximum token transfer fee allowed.
Definition: Protocol.h:81
ripple::sfOwnerCount
const SF_UINT32 sfOwnerCount
ripple::tfTransferable
constexpr const std::uint32_t tfTransferable
Definition: TxFlags.h:130
std::string
STL class.
std::uniform_int_distribution
ripple::maxDeletableTokenOfferEntries
constexpr std::size_t maxDeletableTokenOfferEntries
The maximum number of offers in an offer directory for NFT to be burnable.
Definition: Protocol.h:70
ripple::NFTokenBurn_test::testBurnRandom
void testBurnRandom(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:84
ripple::NFTokenBurn_test::testBurnSequential
void testBurnSequential(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:285
std::vector::reserve
T reserve(T... args)
ripple::TxSearched::all
@ all
Json::UInt
unsigned int UInt
Definition: json_forwards.h:27
std::vector
STL class.
ripple::sfMintedNFTokens
const SF_UINT32 sfMintedNFTokens
ripple::keylet::nftoffer
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition: Indexes.cpp:355
random
ripple::test::jtx::Account::human
std::string const & human() const
Returns the human readable public key.
Definition: Account.h:113
std::reverse
T reverse(T... args)
Json::Value::toStyledString
std::string toStyledString() const
Definition: json_value.cpp:1039
ripple::nft::toTaxon
Taxon toTaxon(std::uint32_t i)
Definition: NFTokenUtils.h:40
ripple::BEAST_DEFINE_TESTSUITE_PRIO
BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 2)
ripple::NFTokenBurn_test
Definition: NFTokenBurn_test.cpp:29
ripple::NFTokenBurn_test::ownerCount
static std::uint32_t ownerCount(test::jtx::Env const &env, test::jtx::Account const &acct)
Definition: NFTokenBurn_test.cpp:33
ripple::SField::jsonName
const Json::StaticString jsonName
Definition: SField.h:136
std::sort
T sort(T... args)
ripple::NFTokenBurn_test::createNftAndOffers
uint256 createNftAndOffers(test::jtx::Env &env, test::jtx::Account const &owner, std::vector< uint256 > &offerIndexes, size_t const tokenCancelCount)
Definition: NFTokenBurn_test.cpp:55
std::mt19937
std::vector::push_back
T push_back(T... args)
std::cout
ripple::Keylet::key
uint256 key
Definition: Keylet.h:40
ripple::base_uint
Integers of any length that is a multiple of 32-bits.
Definition: base_uint.h:81
ripple::tfBurnable
constexpr const std::uint32_t tfBurnable
Definition: TxFlags.h:127
std::to_string
T to_string(T... args)
ripple::test::jtx::Env::close
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:121
Json::Value::size
UInt size() const
Number of values in array or object.
Definition: json_value.cpp:706
ripple::tfSellNFToken
constexpr const std::uint32_t tfSellNFToken
Definition: TxFlags.h:152
Json::Value::isMember
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:932
ripple::maxTokenOfferCancelCount
constexpr std::size_t maxTokenOfferCancelCount
The maximum number of token offers that can be canceled at once.
Definition: Protocol.h:67
std::uint32_t
ripple::test::jtx::Env::seq
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition: Env.cpp:204
ripple::fixNFTokenDirV1
const uint256 fixNFTokenDirV1
ripple::maxTokenURILength
constexpr std::size_t maxTokenURILength
The maximum length of a URI inside an NFT.
Definition: Protocol.h:84
Json::Value::isArray
bool isArray() const
Definition: json_value.cpp:1015
ripple::sfNFTokens
const SField sfNFTokens
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
std::endl
T endl(T... args)
ripple::test::jtx::Env::le
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition: Env.cpp:213
ripple::tefTOO_BIG
@ tefTOO_BIG
Definition: TER.h:163
ripple::FeatureBitset
Definition: Feature.h:113
std::size_t
ripple::to_string
std::string to_string(Manifest const &m)
Format the specified manifest to a string for debugging purposes.
Definition: app/misc/impl/Manifest.cpp:41
ripple::test::jtx::Account
Immutable cryptographic account descriptor.
Definition: Account.h:37
ripple::NFTokenBurn_test::testWithFeats
void testWithFeats(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:774
ripple::fixNonFungibleTokensV1_2
const uint256 fixNonFungibleTokensV1_2
ripple::nft::cipheredTaxon
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition: NFTokenUtils.h:144
ripple::NFTokenBurn_test::nftCount
static std::uint32_t nftCount(test::jtx::Env &env, test::jtx::Account const &acct)
Definition: NFTokenBurn_test.cpp:43
ripple::NFTokenBurn_test::testBurnTooManyOffers
void testBurnTooManyOffers(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:519
ripple::test::jtx::Env
A transaction testing environment.
Definition: Env.h:116
ripple::test::jtx::Env::rpc
Json::Value rpc(std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition: Env.h:687
ripple::NFTokenBurn_test::run
void run() override
Definition: NFTokenBurn_test.cpp:783
Json::Value
Represents a JSON value.
Definition: json_value.h:145
Json::Value::asString
std::string asString() const
Returns the unquoted string value.
Definition: json_value.cpp:469