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