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