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