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 tokenSeq =
384  env.le(acct)->at(~sfMintedNFTokens).value_or(0);
385 
386  // If fixNFTokenRemint amendment is on, we must
387  // add FirstNFTokenSequence.
388  if (env.current()->rules().enabled(fixNFTokenRemint))
389  tokenSeq += env.le(acct)
391  .value_or(env.seq(acct));
392 
393  return toUInt32(
394  nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
395  };
396 
397  for (std::uint32_t i = 0; i < 96; ++i)
398  {
399  // In order to fill the pages we use the taxon to break them
400  // into groups of 16 entries. By having the internal
401  // representation of the taxon go...
402  // 0, 3, 2, 5, 4, 7...
403  // in sets of 16 NFTs we can get each page to be fully
404  // populated.
405  std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
406  uint32_t const extTaxon = internalTaxon(alice, intTaxon);
407  nfts.push_back(token::getNextID(env, alice, extTaxon));
408  env(token::mint(alice, extTaxon));
409  env.close();
410  }
411 
412  // Sort the NFTs so they are listed in storage order, not
413  // creation order.
414  std::sort(nfts.begin(), nfts.end());
415 
416  // Verify that the ledger does indeed contain exactly three pages
417  // of NFTs with 32 entries in each page.
418  Json::Value jvParams;
419  jvParams[jss::ledger_index] = "current";
420  jvParams[jss::binary] = false;
421  {
422  Json::Value jrr = env.rpc(
423  "json",
424  "ledger_data",
425  boost::lexical_cast<std::string>(jvParams));
426 
427  Json::Value& state = jrr[jss::result][jss::state];
428 
429  int pageCount = 0;
430  for (Json::UInt i = 0; i < state.size(); ++i)
431  {
432  if (state[i].isMember(sfNFTokens.jsonName) &&
433  state[i][sfNFTokens.jsonName].isArray())
434  {
435  BEAST_EXPECT(
436  state[i][sfNFTokens.jsonName].size() == 32);
437  ++pageCount;
438  }
439  }
440  // If this check fails then the internal NFT directory logic
441  // has changed.
442  BEAST_EXPECT(pageCount == 3);
443  }
444  };
445 
446  // Generate three packed pages. Then burn the tokens in order from
447  // first to last. This exercises specific cases where coalescing
448  // pages is not possible.
450  genPackedTokens(nfts);
451  BEAST_EXPECT(nftCount(env, alice) == 96);
452  BEAST_EXPECT(ownerCount(env, alice) == 3);
453 
454  for (uint256 const& nft : nfts)
455  {
456  env(token::burn(alice, {nft}));
457  env.close();
458  }
459  BEAST_EXPECT(nftCount(env, alice) == 0);
460  BEAST_EXPECT(ownerCount(env, alice) == 0);
461 
462  // A lambda verifies that the ledger no longer contains any NFT pages.
463  auto checkNoTokenPages = [this, &env]() {
464  Json::Value jvParams;
465  jvParams[jss::ledger_index] = "current";
466  jvParams[jss::binary] = false;
467  {
468  Json::Value jrr = env.rpc(
469  "json",
470  "ledger_data",
471  boost::lexical_cast<std::string>(jvParams));
472 
473  Json::Value& state = jrr[jss::result][jss::state];
474 
475  for (Json::UInt i = 0; i < state.size(); ++i)
476  {
477  BEAST_EXPECT(!state[i].isMember(sfNFTokens.jsonName));
478  }
479  }
480  };
481  checkNoTokenPages();
482 
483  // Generate three packed pages. Then burn the tokens in order from
484  // last to first. This exercises different specific cases where
485  // coalescing pages is not possible.
486  genPackedTokens(nfts);
487  BEAST_EXPECT(nftCount(env, alice) == 96);
488  BEAST_EXPECT(ownerCount(env, alice) == 3);
489 
490  std::reverse(nfts.begin(), nfts.end());
491  for (uint256 const& nft : nfts)
492  {
493  env(token::burn(alice, {nft}));
494  env.close();
495  }
496  BEAST_EXPECT(nftCount(env, alice) == 0);
497  BEAST_EXPECT(ownerCount(env, alice) == 0);
498  checkNoTokenPages();
499 
500  // Generate three packed pages. Then burn all tokens in the middle
501  // page. This exercises the case where a page is removed between
502  // two fully populated pages.
503  genPackedTokens(nfts);
504  BEAST_EXPECT(nftCount(env, alice) == 96);
505  BEAST_EXPECT(ownerCount(env, alice) == 3);
506 
507  for (std::size_t i = 32; i < 64; ++i)
508  {
509  env(token::burn(alice, nfts[i]));
510  env.close();
511  }
512  nfts.erase(nfts.begin() + 32, nfts.begin() + 64);
513  BEAST_EXPECT(nftCount(env, alice) == 64);
514  BEAST_EXPECT(ownerCount(env, alice) == 2);
515 
516  // Burn the remaining nfts.
517  for (uint256 const& nft : nfts)
518  {
519  env(token::burn(alice, {nft}));
520  env.close();
521  }
522  BEAST_EXPECT(nftCount(env, alice) == 0);
523  checkNoTokenPages();
524  }
525 
526  void
528  {
529  // Look at the case where too many offers prevents burning a token.
530  testcase("Burn too many offers");
531 
532  using namespace test::jtx;
533 
534  // Test what happens if a NFT is unburnable when there are
535  // more than 500 offers, before fixNonFungibleTokensV1_2 goes live
536  if (!features[fixNonFungibleTokensV1_2])
537  {
538  Env env{*this, features};
539 
540  Account const alice("alice");
541  Account const becky("becky");
542  env.fund(XRP(1000), alice, becky);
543  env.close();
544 
545  // We structure the test to try and maximize the metadata produced.
546  // This verifies that we don't create too much metadata during a
547  // maximal burn operation.
548  //
549  // 1. alice mints an nft with a full-sized URI.
550  // 2. We create 500 new accounts, each of which creates an offer
551  // for alice's nft.
552  // 3. becky creates one more offer for alice's NFT
553  // 4. Attempt to burn the nft which fails because there are too
554  // many offers.
555  // 5. Cancel becky's offer and the nft should become burnable.
556  uint256 const nftokenID =
557  token::getNextID(env, alice, 0, tfTransferable);
558  env(token::mint(alice, 0),
559  token::uri(std::string(maxTokenURILength, 'u')),
560  txflags(tfTransferable));
561  env.close();
562 
563  std::vector<uint256> offerIndexes;
564  offerIndexes.reserve(maxTokenOfferCancelCount);
565  for (std::uint32_t i = 0; i < maxTokenOfferCancelCount; ++i)
566  {
567  Account const acct(std::string("acct") + std::to_string(i));
568  env.fund(XRP(1000), acct);
569  env.close();
570 
571  offerIndexes.push_back(
572  keylet::nftoffer(acct, env.seq(acct)).key);
573  env(token::createOffer(acct, nftokenID, drops(1)),
574  token::owner(alice));
575  env.close();
576  }
577 
578  // Verify all offers are present in the ledger.
579  for (uint256 const& offerIndex : offerIndexes)
580  {
581  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
582  }
583 
584  // Create one too many offers.
585  uint256 const beckyOfferIndex =
586  keylet::nftoffer(becky, env.seq(becky)).key;
587  env(token::createOffer(becky, nftokenID, drops(1)),
588  token::owner(alice));
589 
590  // Attempt to burn the nft which should fail.
591  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
592 
593  // Close enough ledgers that the burn transaction is no longer
594  // retried.
595  for (int i = 0; i < 10; ++i)
596  env.close();
597 
598  // Cancel becky's offer, but alice adds a sell offer. The token
599  // should still not be burnable.
600  env(token::cancelOffer(becky, {beckyOfferIndex}));
601  env.close();
602 
603  uint256 const aliceOfferIndex =
604  keylet::nftoffer(alice, env.seq(alice)).key;
605  env(token::createOffer(alice, nftokenID, drops(1)),
606  txflags(tfSellNFToken));
607  env.close();
608 
609  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
610  env.close();
611 
612  // Cancel alice's sell offer. Now the token should be burnable.
613  env(token::cancelOffer(alice, {aliceOfferIndex}));
614  env.close();
615 
616  env(token::burn(alice, nftokenID));
617  env.close();
618 
619  // Burning the token should remove all the offers from the ledger.
620  for (uint256 const& offerIndex : offerIndexes)
621  {
622  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
623  }
624 
625  // Both alice and becky should have ownerCounts of zero.
626  BEAST_EXPECT(ownerCount(env, alice) == 0);
627  BEAST_EXPECT(ownerCount(env, becky) == 0);
628  }
629 
630  // Test that up to 499 buy/sell offers will be removed when NFT is
631  // burned after fixNonFungibleTokensV1_2 is enabled. This is to test
632  // that we can successfully remove all offers if the number of offers is
633  // less than 500.
634  if (features[fixNonFungibleTokensV1_2])
635  {
636  Env env{*this, features};
637 
638  Account const alice("alice");
639  Account const becky("becky");
640  env.fund(XRP(100000), alice, becky);
641  env.close();
642 
643  // alice creates 498 sell offers and becky creates 1 buy offers.
644  // When the token is burned, 498 sell offers and 1 buy offer are
645  // removed. In total, 499 offers are removed
646  std::vector<uint256> offerIndexes;
647  auto const nftokenID = createNftAndOffers(
648  env, alice, offerIndexes, maxDeletableTokenOfferEntries - 2);
649 
650  // Verify all sell offers are present in the ledger.
651  for (uint256 const& offerIndex : offerIndexes)
652  {
653  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
654  }
655 
656  // Becky creates a buy offer
657  uint256 const beckyOfferIndex =
658  keylet::nftoffer(becky, env.seq(becky)).key;
659  env(token::createOffer(becky, nftokenID, drops(1)),
660  token::owner(alice));
661  env.close();
662 
663  // Burn the token
664  env(token::burn(alice, nftokenID));
665  env.close();
666 
667  // Burning the token should remove all 498 sell offers
668  // that alice created
669  for (uint256 const& offerIndex : offerIndexes)
670  {
671  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
672  }
673 
674  // Burning the token should also remove the one buy offer
675  // that becky created
676  BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex)));
677 
678  // alice and becky should have ownerCounts of zero
679  BEAST_EXPECT(ownerCount(env, alice) == 0);
680  BEAST_EXPECT(ownerCount(env, becky) == 0);
681  }
682 
683  // Test that up to 500 buy offers are removed when NFT is burned
684  // after fixNonFungibleTokensV1_2 is enabled
685  if (features[fixNonFungibleTokensV1_2])
686  {
687  Env env{*this, features};
688 
689  Account const alice("alice");
690  Account const becky("becky");
691  env.fund(XRP(100000), alice, becky);
692  env.close();
693 
694  // alice creates 501 sell offers for the token
695  // After we burn the token, 500 of the sell offers should be
696  // removed, and one is left over
697  std::vector<uint256> offerIndexes;
698  auto const nftokenID = createNftAndOffers(
699  env, alice, offerIndexes, maxDeletableTokenOfferEntries + 1);
700 
701  // Verify all sell offers are present in the ledger.
702  for (uint256 const& offerIndex : offerIndexes)
703  {
704  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
705  }
706 
707  // Burn the token
708  env(token::burn(alice, nftokenID));
709  env.close();
710 
711  uint32_t offerDeletedCount = 0;
712  // Count the number of sell offers that have been deleted
713  for (uint256 const& offerIndex : offerIndexes)
714  {
715  if (!env.le(keylet::nftoffer(offerIndex)))
716  offerDeletedCount++;
717  }
718 
719  BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1);
720 
721  // 500 sell offers should be removed
722  BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount);
723 
724  // alice should have ownerCounts of one for the orphaned sell offer
725  BEAST_EXPECT(ownerCount(env, alice) == 1);
726  }
727 
728  // Test that up to 500 buy/sell offers are removed when NFT is burned
729  // after fixNonFungibleTokensV1_2 is enabled
730  if (features[fixNonFungibleTokensV1_2])
731  {
732  Env env{*this, features};
733 
734  Account const alice("alice");
735  Account const becky("becky");
736  env.fund(XRP(100000), alice, becky);
737  env.close();
738 
739  // alice creates 499 sell offers and becky creates 2 buy offers.
740  // When the token is burned, 499 sell offers and 1 buy offer
741  // are removed.
742  // In total, 500 offers are removed
743  std::vector<uint256> offerIndexes;
744  auto const nftokenID = createNftAndOffers(
745  env, alice, offerIndexes, maxDeletableTokenOfferEntries - 1);
746 
747  // Verify all sell offers are present in the ledger.
748  for (uint256 const& offerIndex : offerIndexes)
749  {
750  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
751  }
752 
753  // becky creates 2 buy offers
754  env(token::createOffer(becky, nftokenID, drops(1)),
755  token::owner(alice));
756  env.close();
757  env(token::createOffer(becky, nftokenID, drops(1)),
758  token::owner(alice));
759  env.close();
760 
761  // Burn the token
762  env(token::burn(alice, nftokenID));
763  env.close();
764 
765  // Burning the token should remove all 499 sell offers from the
766  // ledger.
767  for (uint256 const& offerIndex : offerIndexes)
768  {
769  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
770  }
771 
772  // alice should have ownerCount of zero because all her
773  // sell offers have been deleted
774  BEAST_EXPECT(ownerCount(env, alice) == 0);
775 
776  // becky has ownerCount of one due to an orphaned buy offer
777  BEAST_EXPECT(ownerCount(env, becky) == 1);
778  }
779  }
780 
781  void
783  {
784  testBurnRandom(features);
785  testBurnSequential(features);
786  testBurnTooManyOffers(features);
787  }
788 
789 public:
790  void
791  run() override
792  {
793  using namespace test::jtx;
794  FeatureBitset const all{supported_amendments()};
795  FeatureBitset const fixNFTDir{fixNFTokenDirV1};
796 
802  }
803 };
804 
805 BEAST_DEFINE_TESTSUITE_PRIO(NFTokenBurn, tx, ripple, 3);
806 
807 } // 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::sfFirstNFTokenSequence
const SF_UINT32 sfFirstNFTokenSequence
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
ripple::fixNFTokenRemint
const uint256 fixNFTokenRemint
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< 256 >
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:207
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:216
ripple::tefTOO_BIG
@ tefTOO_BIG
Definition: TER.h:166
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:782
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:527
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:791
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