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  void
54  {
55  // Exercise a number of conditions with NFT burning.
56  testcase("Burn random");
57 
58  using namespace test::jtx;
59 
60  Env env{*this, features};
61 
62  // Keep information associated with each account together.
63  struct AcctStat
64  {
65  test::jtx::Account const acct;
67 
68  AcctStat(char const* name) : acct(name)
69  {
70  }
71 
72  operator test::jtx::Account() const
73  {
74  return acct;
75  }
76  };
77  AcctStat alice{"alice"};
78  AcctStat becky{"becky"};
79  AcctStat minter{"minter"};
80 
81  env.fund(XRP(10000), alice, becky, minter);
82  env.close();
83 
84  // Both alice and minter mint nfts in case that makes any difference.
85  env(token::setMinter(alice, minter));
86  env.close();
87 
88  // Create enough NFTs that alice, becky, and minter can all have
89  // at least three pages of NFTs. This will cause more activity in
90  // the page coalescing code. If we make 210 NFTs in total, we can
91  // have alice and minter each make 105. That will allow us to
92  // distribute 70 NFTs to our three participants.
93  //
94  // Give each NFT a pseudo-randomly chosen fee so the NFTs are
95  // distributed pseudo-randomly through the pages. This should
96  // prevent alice's and minter's NFTs from clustering together
97  // in becky's directory.
98  //
99  // Use a default initialized mercenne_twister because we want the
100  // effect of random numbers, but we want the test to run the same
101  // way each time.
102  std::mt19937 engine;
104  decltype(maxTransferFee){}, maxTransferFee);
105 
106  alice.nfts.reserve(105);
107  while (alice.nfts.size() < 105)
108  {
109  std::uint16_t const xferFee = feeDist(engine);
110  alice.nfts.push_back(token::getNextID(
111  env, alice, 0u, tfTransferable | tfBurnable, xferFee));
112  env(token::mint(alice),
113  txflags(tfTransferable | tfBurnable),
114  token::xferFee(xferFee));
115  env.close();
116  }
117 
118  minter.nfts.reserve(105);
119  while (minter.nfts.size() < 105)
120  {
121  std::uint16_t const xferFee = feeDist(engine);
122  minter.nfts.push_back(token::getNextID(
123  env, alice, 0u, tfTransferable | tfBurnable, xferFee));
124  env(token::mint(minter),
125  txflags(tfTransferable | tfBurnable),
126  token::xferFee(xferFee),
127  token::issuer(alice));
128  env.close();
129  }
130 
131  // All of the NFTs are now minted. Transfer 35 each over to becky so
132  // we end up with 70 NFTs in each account.
133  becky.nfts.reserve(70);
134  {
135  auto aliceIter = alice.nfts.begin();
136  auto minterIter = minter.nfts.begin();
137  while (becky.nfts.size() < 70)
138  {
139  // We do the same work on alice and minter, so make a lambda.
140  auto xferNFT = [&env, &becky](AcctStat& acct, auto& iter) {
141  uint256 offerIndex =
142  keylet::nftoffer(acct.acct, env.seq(acct.acct)).key;
143  env(token::createOffer(acct, *iter, XRP(0)),
144  txflags(tfSellNFToken));
145  env.close();
146  env(token::acceptSellOffer(becky, offerIndex));
147  env.close();
148  becky.nfts.push_back(*iter);
149  iter = acct.nfts.erase(iter);
150  iter += 2;
151  };
152  xferNFT(alice, aliceIter);
153  xferNFT(minter, minterIter);
154  }
155  BEAST_EXPECT(aliceIter == alice.nfts.end());
156  BEAST_EXPECT(minterIter == minter.nfts.end());
157  }
158 
159  // Now all three participants have 70 NFTs.
160  BEAST_EXPECT(nftCount(env, alice.acct) == 70);
161  BEAST_EXPECT(nftCount(env, becky.acct) == 70);
162  BEAST_EXPECT(nftCount(env, minter.acct) == 70);
163 
164  // Next we'll create offers for all of those NFTs. This calls for
165  // another lambda.
166  auto addOffers =
167  [&env](AcctStat& owner, AcctStat& other1, AcctStat& other2) {
168  for (uint256 nft : owner.nfts)
169  {
170  // Create sell offers for owner.
171  env(token::createOffer(owner, nft, drops(1)),
172  txflags(tfSellNFToken),
173  token::destination(other1));
174  env(token::createOffer(owner, nft, drops(1)),
175  txflags(tfSellNFToken),
176  token::destination(other2));
177  env.close();
178 
179  // Create buy offers for other1 and other2.
180  env(token::createOffer(other1, nft, drops(1)),
181  token::owner(owner));
182  env(token::createOffer(other2, nft, drops(1)),
183  token::owner(owner));
184  env.close();
185 
186  env(token::createOffer(other2, nft, drops(2)),
187  token::owner(owner));
188  env(token::createOffer(other1, nft, drops(2)),
189  token::owner(owner));
190  env.close();
191  }
192  };
193  addOffers(alice, becky, minter);
194  addOffers(becky, minter, alice);
195  addOffers(minter, alice, becky);
196  BEAST_EXPECT(ownerCount(env, alice) == 424);
197  BEAST_EXPECT(ownerCount(env, becky) == 424);
198  BEAST_EXPECT(ownerCount(env, minter) == 424);
199 
200  // Now each of the 270 NFTs has six offers associated with it.
201  // Randomly select an NFT out of the pile and burn it. Continue
202  // the process until all NFTs are burned.
203  AcctStat* const stats[3] = {&alice, &becky, &minter};
206 
207  while (stats[0]->nfts.size() > 0 || stats[1]->nfts.size() > 0 ||
208  stats[2]->nfts.size() > 0)
209  {
210  // Pick an account to burn an nft. If there are no nfts left
211  // pick again.
212  AcctStat& owner = *(stats[acctDist(engine)]);
213  if (owner.nfts.empty())
214  continue;
215 
216  // Pick one of the nfts.
218  0lu, owner.nfts.size() - 1);
219  auto nftIter = owner.nfts.begin() + nftDist(engine);
220  uint256 const nft = *nftIter;
221  owner.nfts.erase(nftIter);
222 
223  // Decide which of the accounts should burn the nft. If the
224  // owner is becky then any of the three accounts can burn.
225  // Otherwise either alice or minter can burn.
226  AcctStat& burner = owner.acct == becky.acct
227  ? *(stats[acctDist(engine)])
228  : mintDist(engine) ? alice : minter;
229 
230  if (owner.acct == burner.acct)
231  env(token::burn(burner, nft));
232  else
233  env(token::burn(burner, nft), token::owner(owner));
234  env.close();
235 
236  // Every time we burn an nft, the number of nfts they hold should
237  // match the number of nfts we think they hold.
238  BEAST_EXPECT(nftCount(env, alice.acct) == alice.nfts.size());
239  BEAST_EXPECT(nftCount(env, becky.acct) == becky.nfts.size());
240  BEAST_EXPECT(nftCount(env, minter.acct) == minter.nfts.size());
241  }
242  BEAST_EXPECT(nftCount(env, alice.acct) == 0);
243  BEAST_EXPECT(nftCount(env, becky.acct) == 0);
244  BEAST_EXPECT(nftCount(env, minter.acct) == 0);
245 
246  // When all nfts are burned none of the accounts should have
247  // an ownerCount.
248  BEAST_EXPECT(ownerCount(env, alice) == 0);
249  BEAST_EXPECT(ownerCount(env, becky) == 0);
250  BEAST_EXPECT(ownerCount(env, minter) == 0);
251  }
252 
253  void
255  {
256  // The earlier burn test randomizes which nft is burned. There are
257  // a couple of directory merging scenarios that can only be tested by
258  // inserting and deleting in an ordered fashion. We do that testing
259  // now.
260  testcase("Burn sequential");
261 
262  using namespace test::jtx;
263 
264  Account const alice{"alice"};
265 
266  Env env{*this, features};
267  env.fund(XRP(1000), alice);
268 
269  // printNFTPages is a lambda that may be used for debugging.
270  //
271  // It uses the ledger RPC command to show the NFT pages in the ledger.
272  // This parameter controls how noisy the output is.
273  enum Volume : bool {
274  quiet = false,
275  noisy = true,
276  };
277 
278  [[maybe_unused]] auto printNFTPages = [&env](Volume vol) {
279  Json::Value jvParams;
280  jvParams[jss::ledger_index] = "current";
281  jvParams[jss::binary] = false;
282  {
283  Json::Value jrr = env.rpc(
284  "json",
285  "ledger_data",
286  boost::lexical_cast<std::string>(jvParams));
287 
288  // Iterate the state and print all NFTokenPages.
289  if (!jrr.isMember(jss::result) ||
290  !jrr[jss::result].isMember(jss::state))
291  {
292  std::cout << "No ledger state found!" << std::endl;
293  return;
294  }
295  Json::Value& state = jrr[jss::result][jss::state];
296  if (!state.isArray())
297  {
298  std::cout << "Ledger state is not array!" << std::endl;
299  return;
300  }
301  for (Json::UInt i = 0; i < state.size(); ++i)
302  {
303  if (state[i].isMember(sfNFTokens.jsonName) &&
304  state[i][sfNFTokens.jsonName].isArray())
305  {
306  std::uint32_t tokenCount =
307  state[i][sfNFTokens.jsonName].size();
308  std::cout << tokenCount << " NFTokens in page "
309  << state[i][jss::index].asString()
310  << std::endl;
311 
312  if (vol == noisy)
313  {
314  std::cout << state[i].toStyledString() << std::endl;
315  }
316  else
317  {
318  if (tokenCount > 0)
319  std::cout << "first: "
320  << state[i][sfNFTokens.jsonName][0u]
321  .toStyledString()
322  << std::endl;
323  if (tokenCount > 1)
324  std::cout << "last: "
325  << state[i][sfNFTokens.jsonName]
326  [tokenCount - 1]
327  .toStyledString()
328  << std::endl;
329  }
330  }
331  }
332  }
333  };
334 
335  // A lambda that generates 96 nfts packed into three pages of 32 each.
336  auto genPackedTokens = [this, &env, &alice](
337  std::vector<uint256>& nfts) {
338  nfts.clear();
339  nfts.reserve(96);
340 
341  // We want to create fully packed NFT pages. This is a little
342  // tricky since the system currently in place is inclined to
343  // assign consecutive tokens to only 16 entries per page.
344  //
345  // By manipulating the internal form of the taxon we can force
346  // creation of NFT pages that are completely full. This lambda
347  // tells us the taxon value we should pass in in order for the
348  // internal representation to match the passed in value.
349  auto internalTaxon = [&env](
350  Account const& acct,
351  std::uint32_t taxon) -> std::uint32_t {
352  std::uint32_t const tokenSeq = {
353  env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
354  return toUInt32(
355  nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
356  };
357 
358  for (std::uint32_t i = 0; i < 96; ++i)
359  {
360  // In order to fill the pages we use the taxon to break them
361  // into groups of 16 entries. By having the internal
362  // representation of the taxon go...
363  // 0, 3, 2, 5, 4, 7...
364  // in sets of 16 NFTs we can get each page to be fully
365  // populated.
366  std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
367  uint32_t const extTaxon = internalTaxon(alice, intTaxon);
368  nfts.push_back(token::getNextID(env, alice, extTaxon));
369  env(token::mint(alice, extTaxon));
370  env.close();
371  }
372 
373  // Sort the NFTs so they are listed in storage order, not
374  // creation order.
375  std::sort(nfts.begin(), nfts.end());
376 
377  // Verify that the ledger does indeed contain exactly three pages
378  // of NFTs with 32 entries in each page.
379  Json::Value jvParams;
380  jvParams[jss::ledger_index] = "current";
381  jvParams[jss::binary] = false;
382  {
383  Json::Value jrr = env.rpc(
384  "json",
385  "ledger_data",
386  boost::lexical_cast<std::string>(jvParams));
387 
388  Json::Value& state = jrr[jss::result][jss::state];
389 
390  int pageCount = 0;
391  for (Json::UInt i = 0; i < state.size(); ++i)
392  {
393  if (state[i].isMember(sfNFTokens.jsonName) &&
394  state[i][sfNFTokens.jsonName].isArray())
395  {
396  BEAST_EXPECT(
397  state[i][sfNFTokens.jsonName].size() == 32);
398  ++pageCount;
399  }
400  }
401  // If this check fails then the internal NFT directory logic
402  // has changed.
403  BEAST_EXPECT(pageCount == 3);
404  }
405  };
406 
407  // Generate three packed pages. Then burn the tokens in order from
408  // first to last. This exercises specific cases where coalescing
409  // pages is not possible.
411  genPackedTokens(nfts);
412  BEAST_EXPECT(nftCount(env, alice) == 96);
413  BEAST_EXPECT(ownerCount(env, alice) == 3);
414 
415  for (uint256 const& nft : nfts)
416  {
417  env(token::burn(alice, {nft}));
418  env.close();
419  }
420  BEAST_EXPECT(nftCount(env, alice) == 0);
421  BEAST_EXPECT(ownerCount(env, alice) == 0);
422 
423  // A lambda verifies that the ledger no longer contains any NFT pages.
424  auto checkNoTokenPages = [this, &env]() {
425  Json::Value jvParams;
426  jvParams[jss::ledger_index] = "current";
427  jvParams[jss::binary] = false;
428  {
429  Json::Value jrr = env.rpc(
430  "json",
431  "ledger_data",
432  boost::lexical_cast<std::string>(jvParams));
433 
434  Json::Value& state = jrr[jss::result][jss::state];
435 
436  for (Json::UInt i = 0; i < state.size(); ++i)
437  {
438  BEAST_EXPECT(!state[i].isMember(sfNFTokens.jsonName));
439  }
440  }
441  };
442  checkNoTokenPages();
443 
444  // Generate three packed pages. Then burn the tokens in order from
445  // last to first. This exercises different specific cases where
446  // coalescing pages is not possible.
447  genPackedTokens(nfts);
448  BEAST_EXPECT(nftCount(env, alice) == 96);
449  BEAST_EXPECT(ownerCount(env, alice) == 3);
450 
451  std::reverse(nfts.begin(), nfts.end());
452  for (uint256 const& nft : nfts)
453  {
454  env(token::burn(alice, {nft}));
455  env.close();
456  }
457  BEAST_EXPECT(nftCount(env, alice) == 0);
458  BEAST_EXPECT(ownerCount(env, alice) == 0);
459  checkNoTokenPages();
460 
461  // Generate three packed pages. Then burn all tokens in the middle
462  // page. This exercises the case where a page is removed between
463  // two fully populated pages.
464  genPackedTokens(nfts);
465  BEAST_EXPECT(nftCount(env, alice) == 96);
466  BEAST_EXPECT(ownerCount(env, alice) == 3);
467 
468  for (std::size_t i = 32; i < 64; ++i)
469  {
470  env(token::burn(alice, nfts[i]));
471  env.close();
472  }
473  nfts.erase(nfts.begin() + 32, nfts.begin() + 64);
474  BEAST_EXPECT(nftCount(env, alice) == 64);
475  BEAST_EXPECT(ownerCount(env, alice) == 2);
476 
477  // Burn the remaining nfts.
478  for (uint256 const& nft : nfts)
479  {
480  env(token::burn(alice, {nft}));
481  env.close();
482  }
483  BEAST_EXPECT(nftCount(env, alice) == 0);
484  checkNoTokenPages();
485  }
486 
487  void
489  {
490  // Look at the case where too many offers prevents burning a token.
491  testcase("Burn too many offers");
492 
493  using namespace test::jtx;
494 
495  Env env{*this, features};
496 
497  Account const alice("alice");
498  Account const becky("becky");
499  env.fund(XRP(1000), alice, becky);
500  env.close();
501 
502  // We structure the test to try and maximize the metadata produced.
503  // This verifies that we don't create too much metadata during a
504  // maximal burn operation.
505  //
506  // 1. alice mints an nft with a full-sized URI.
507  // 2. We create 1000 new accounts, each of which creates an offer for
508  // alice's nft.
509  // 3. becky creates one more offer for alice's NFT
510  // 4. Attempt to burn the nft which fails because there are too
511  // many offers.
512  // 5. Cancel becky's offer and the nft should become burnable.
513  uint256 const nftokenID =
514  token::getNextID(env, alice, 0, tfTransferable);
515  env(token::mint(alice, 0),
516  token::uri(std::string(maxTokenURILength, 'u')),
517  txflags(tfTransferable));
518  env.close();
519 
520  std::vector<uint256> offerIndexes;
521  offerIndexes.reserve(maxTokenOfferCancelCount);
522  for (uint32_t i = 0; i < maxTokenOfferCancelCount; ++i)
523  {
524  Account const acct(std::string("acct") + std::to_string(i));
525  env.fund(XRP(1000), acct);
526  env.close();
527 
528  offerIndexes.push_back(keylet::nftoffer(acct, env.seq(acct)).key);
529  env(token::createOffer(acct, nftokenID, drops(1)),
530  token::owner(alice));
531  env.close();
532  }
533 
534  // Verify all offers are present in the ledger.
535  for (uint256 const& offerIndex : offerIndexes)
536  {
537  BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
538  }
539 
540  // Create one too many offers.
541  uint256 const beckyOfferIndex =
542  keylet::nftoffer(becky, env.seq(becky)).key;
543  env(token::createOffer(becky, nftokenID, drops(1)),
544  token::owner(alice));
545 
546  // Attempt to burn the nft which should fail.
547  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
548 
549  // Close enough ledgers that the burn transaction is no longer retried.
550  for (int i = 0; i < 10; ++i)
551  env.close();
552 
553  // Cancel becky's offer, but alice adds a sell offer. The token
554  // should still not be burnable.
555  env(token::cancelOffer(becky, {beckyOfferIndex}));
556  env.close();
557 
558  uint256 const aliceOfferIndex =
559  keylet::nftoffer(alice, env.seq(alice)).key;
560  env(token::createOffer(alice, nftokenID, drops(1)),
561  txflags(tfSellNFToken));
562  env.close();
563 
564  env(token::burn(alice, nftokenID), ter(tefTOO_BIG));
565  env.close();
566 
567  // Cancel alice's sell offer. Now the token should be burnable.
568  env(token::cancelOffer(alice, {aliceOfferIndex}));
569  env.close();
570 
571  env(token::burn(alice, nftokenID));
572  env.close();
573 
574  // Burning the token should remove all the offers from the ledger.
575  for (uint256 const& offerIndex : offerIndexes)
576  {
577  BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
578  }
579 
580  // Both alice and becky should have ownerCounts of zero.
581  BEAST_EXPECT(ownerCount(env, alice) == 0);
582  BEAST_EXPECT(ownerCount(env, becky) == 0);
583  }
584 
585  void
587  {
588  testBurnRandom(features);
589  testBurnSequential(features);
590  testBurnTooManyOffers(features);
591  }
592 
593 public:
594  void
595  run() override
596  {
597  using namespace test::jtx;
598  FeatureBitset const all{supported_amendments()};
599  FeatureBitset const fixNFTDir{fixNFTokenDirV1};
600 
601  testWithFeats(all - fixNFTDir);
603  }
604 };
605 
606 BEAST_DEFINE_TESTSUITE_PRIO(NFTokenBurn, tx, ripple, 3);
607 
608 } // 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:121
std::string
STL class.
std::uniform_int_distribution
ripple::NFTokenBurn_test::testBurnRandom
void testBurnRandom(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:53
ripple::NFTokenBurn_test::testBurnSequential
void testBurnSequential(FeatureBitset features)
Definition: NFTokenBurn_test.cpp:254
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)
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:118
std::to_string
T to_string(T... args)
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:143
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::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:586
ripple::nft::cipheredTaxon
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition: NFTokenUtils.h:140
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:488
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:684
ripple::NFTokenBurn_test::run
void run() override
Definition: NFTokenBurn_test.cpp:595
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