rippled
Loading...
Searching...
No Matches
FixNFTokenPageLinks_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2024 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/ApplyContext.h>
23#include <xrpld/app/tx/detail/NFTokenUtils.h>
24
25#include <xrpl/protocol/Feature.h>
26#include <xrpl/protocol/jss.h>
27
28namespace ripple {
29
31{
32 // Helper function that returns the number of nfts owned by an account.
33 static std::uint32_t
35 {
36 Json::Value params;
37 params[jss::account] = acct.human();
38 params[jss::type] = "state";
39 Json::Value nfts = env.rpc("json", "account_nfts", to_string(params));
40 return nfts[jss::result][jss::account_nfts].size();
41 };
42
43 // A helper function that generates 96 nfts packed into three pages
44 // of 32 each. Returns a sorted vector of the NFTokenIDs packed into
45 // the pages.
48 {
49 using namespace test::jtx;
50
52 nfts.reserve(96);
53
54 // We want to create fully packed NFT pages. This is a little
55 // tricky since the system currently in place is inclined to
56 // assign consecutive tokens to only 16 entries per page.
57 //
58 // By manipulating the internal form of the taxon we can force
59 // creation of NFT pages that are completely full. This lambda
60 // tells us the taxon value we should pass in in order for the
61 // internal representation to match the passed in value.
62 auto internalTaxon = [this, &env](
63 Account const& acct,
65 std::uint32_t tokenSeq = [this, &env, &acct]() {
66 auto const le = env.le(acct);
67 if (BEAST_EXPECT(le))
68 return le->at(~sfMintedNFTokens).value_or(0u);
69 return 0u;
70 }();
71
72 // We must add FirstNFTokenSequence.
73 tokenSeq += env.le(acct)
74 ->at(~sfFirstNFTokenSequence)
75 .value_or(env.seq(acct));
76
77 return toUInt32(nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
78 };
79
80 for (std::uint32_t i = 0; i < 96; ++i)
81 {
82 // In order to fill the pages we use the taxon to break them
83 // into groups of 16 entries. By having the internal
84 // representation of the taxon go...
85 // 0, 3, 2, 5, 4, 7...
86 // in sets of 16 NFTs we can get each page to be fully
87 // populated.
88 std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
89 uint32_t const extTaxon = internalTaxon(owner, intTaxon);
90 nfts.push_back(
91 token::getNextID(env, owner, extTaxon, tfTransferable));
92 env(token::mint(owner, extTaxon), txflags(tfTransferable));
93 env.close();
94 }
95
96 // Sort the NFTs so they are listed in storage order, not
97 // creation order.
98 std::sort(nfts.begin(), nfts.end());
99
100 // Verify that the owner does indeed have exactly three pages
101 // of NFTs with 32 entries in each page.
102 {
103 Json::Value params;
104 params[jss::account] = owner.human();
105 auto resp = env.rpc("json", "account_objects", to_string(params));
106
107 Json::Value const& acctObjs =
108 resp[jss::result][jss::account_objects];
109
110 int pageCount = 0;
111 for (Json::UInt i = 0; i < acctObjs.size(); ++i)
112 {
113 if (BEAST_EXPECT(
114 acctObjs[i].isMember(sfNFTokens.jsonName) &&
115 acctObjs[i][sfNFTokens.jsonName].isArray()))
116 {
117 BEAST_EXPECT(acctObjs[i][sfNFTokens.jsonName].size() == 32);
118 ++pageCount;
119 }
120 }
121 // If this check fails then the internal NFT directory logic
122 // has changed.
123 BEAST_EXPECT(pageCount == 3);
124 }
125 return nfts;
126 };
127
128 void
130 {
131 testcase("LedgerStateFix error cases");
132
133 using namespace test::jtx;
134
135 Account const alice("alice");
136
137 {
138 // Verify that the LedgerStateFix transaction is disabled
139 // without the fixNFTokenPageLinks amendment.
140 Env env{*this, testable_amendments() - fixNFTokenPageLinks};
141 env.fund(XRP(1000), alice);
142
143 auto const linkFixFee = drops(env.current()->fees().increment);
144 env(ledgerStateFix::nftPageLinks(alice, alice),
145 fee(linkFixFee),
146 ter(temDISABLED));
147 }
148
149 Env env{*this, testable_amendments()};
150 env.fund(XRP(1000), alice);
151 std::uint32_t const ticketSeq = env.seq(alice);
152 env(ticket::create(alice, 1));
153
154 // Preflight
155
156 {
157 // Fail preflight1. Can't combine AcccountTxnID and ticket.
158 Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
159 tx[sfAccountTxnID.jsonName] =
160 "00000000000000000000000000000000"
161 "00000000000000000000000000000000";
162 env(tx, ticket::use(ticketSeq), ter(temINVALID));
163 }
164 // Fee too low.
165 env(ledgerStateFix::nftPageLinks(alice, alice), ter(telINSUF_FEE_P));
166
167 // Invalid flags.
168 auto const linkFixFee = drops(env.current()->fees().increment);
169 env(ledgerStateFix::nftPageLinks(alice, alice),
170 fee(linkFixFee),
171 txflags(tfPassive),
172 ter(temINVALID_FLAG));
173
174 {
175 // ledgerStateFix::nftPageLinks requires an Owner field.
176 Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
177 tx.removeMember(sfOwner.jsonName);
178 env(tx, fee(linkFixFee), ter(temINVALID));
179 }
180 {
181 // Invalid LedgerFixType codes.
182 Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
183 tx[sfLedgerFixType.jsonName] = 0;
184 env(tx, fee(linkFixFee), ter(tefINVALID_LEDGER_FIX_TYPE));
185
186 tx[sfLedgerFixType.jsonName] = 200;
187 env(tx, fee(linkFixFee), ter(tefINVALID_LEDGER_FIX_TYPE));
188 }
189
190 // Preclaim
191 Account const carol("carol");
192 env.memoize(carol);
193 env(ledgerStateFix::nftPageLinks(alice, carol),
194 fee(linkFixFee),
196 }
197
198 void
200 {
201 testcase("NFTokenPageLinkFix error cases");
202
203 using namespace test::jtx;
204
205 Account const alice("alice");
206
207 Env env{*this, testable_amendments()};
208 env.fund(XRP(1000), alice);
209
210 // These cases all return the same TER code, but they exercise
211 // different cases where there is nothing to fix in an owner's
212 // NFToken pages. So they increase test coverage.
213
214 // Owner has no pages to fix.
215 auto const linkFixFee = drops(env.current()->fees().increment);
216 env(ledgerStateFix::nftPageLinks(alice, alice),
217 fee(linkFixFee),
219
220 // Alice has only one page.
221 env(token::mint(alice), txflags(tfTransferable));
222 env.close();
223
224 env(ledgerStateFix::nftPageLinks(alice, alice),
225 fee(linkFixFee),
227
228 // Alice has at least three pages.
229 for (std::uint32_t i = 0; i < 64; ++i)
230 {
231 env(token::mint(alice), txflags(tfTransferable));
232 env.close();
233 }
234
235 env(ledgerStateFix::nftPageLinks(alice, alice),
236 fee(linkFixFee),
238 }
239
240 void
242 {
243 // Steps:
244 // 1. Before the fixNFTokenPageLinks amendment is enabled, build the
245 // three kinds of damaged NFToken directories we know about:
246 // A. One where there is only one page, but without the final index.
247 // B. One with multiple pages and a missing final page.
248 // C. One with links missing in the middle of the chain.
249 // 2. Enable the fixNFTokenPageLinks amendment.
250 // 3. Invoke the LedgerStateFix transactor and repair the directories.
251 testcase("Fix links");
252
253 using namespace test::jtx;
254
255 Account const alice("alice");
256 Account const bob("bob");
257 Account const carol("carol");
258 Account const daria("daria");
259
260 Env env{*this, testable_amendments() - fixNFTokenPageLinks};
261 env.fund(XRP(1000), alice, bob, carol, daria);
262
263 //**********************************************************************
264 // Step 1A: Create damaged NFToken directories:
265 // o One where there is only one page, but without the final index.
266 //**********************************************************************
267
268 // alice generates three packed pages.
269 std::vector<uint256> aliceNFTs = genPackedTokens(env, alice);
270 BEAST_EXPECT(nftCount(env, alice) == 96);
271 BEAST_EXPECT(ownerCount(env, alice) == 3);
272
273 // Get the index of the middle page.
274 uint256 const aliceMiddleNFTokenPageIndex = [&env, &alice]() {
275 auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
276 return lastNFTokenPage->at(sfPreviousPageMin);
277 }();
278
279 // alice burns all the tokens in the first and last pages.
280 for (int i = 0; i < 32; ++i)
281 {
282 env(token::burn(alice, {aliceNFTs[i]}));
283 env.close();
284 }
285 aliceNFTs.erase(aliceNFTs.begin(), aliceNFTs.begin() + 32);
286 for (int i = 0; i < 32; ++i)
287 {
288 env(token::burn(alice, {aliceNFTs.back()}));
289 aliceNFTs.pop_back();
290 env.close();
291 }
292 BEAST_EXPECT(ownerCount(env, alice) == 1);
293 BEAST_EXPECT(nftCount(env, alice) == 32);
294
295 // Removing the last token from the last page deletes the last
296 // page. This is a bug. The contents of the next-to-last page
297 // should have been moved into the last page.
298 BEAST_EXPECT(!env.le(keylet::nftpage_max(alice)));
299
300 // alice's "middle" page is still present, but has no links.
301 {
302 auto aliceMiddleNFTokenPage = env.le(keylet::nftpage(
303 keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex));
304 if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
305 return;
306
307 BEAST_EXPECT(
308 !aliceMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
309 BEAST_EXPECT(
310 !aliceMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
311 }
312
313 //**********************************************************************
314 // Step 1B: Create damaged NFToken directories:
315 // o One with multiple pages and a missing final page.
316 //**********************************************************************
317
318 // bob generates three packed pages.
319 std::vector<uint256> bobNFTs = genPackedTokens(env, bob);
320 BEAST_EXPECT(nftCount(env, bob) == 96);
321 BEAST_EXPECT(ownerCount(env, bob) == 3);
322
323 // Get the index of the middle page.
324 uint256 const bobMiddleNFTokenPageIndex = [&env, &bob]() {
325 auto lastNFTokenPage = env.le(keylet::nftpage_max(bob));
326 return lastNFTokenPage->at(sfPreviousPageMin);
327 }();
328
329 // bob burns all the tokens in the very last page.
330 for (int i = 0; i < 32; ++i)
331 {
332 env(token::burn(bob, {bobNFTs.back()}));
333 bobNFTs.pop_back();
334 env.close();
335 }
336 BEAST_EXPECT(nftCount(env, bob) == 64);
337 BEAST_EXPECT(ownerCount(env, bob) == 2);
338
339 // Removing the last token from the last page deletes the last
340 // page. This is a bug. The contents of the next-to-last page
341 // should have been moved into the last page.
342 BEAST_EXPECT(!env.le(keylet::nftpage_max(bob)));
343
344 // bob's "middle" page is still present, but has lost the
345 // NextPageMin field.
346 {
347 auto bobMiddleNFTokenPage = env.le(keylet::nftpage(
348 keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex));
349 if (!BEAST_EXPECT(bobMiddleNFTokenPage))
350 return;
351
352 BEAST_EXPECT(
353 bobMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
354 BEAST_EXPECT(!bobMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
355 }
356
357 //**********************************************************************
358 // Step 1C: Create damaged NFToken directories:
359 // o One with links missing in the middle of the chain.
360 //**********************************************************************
361
362 // carol generates three packed pages.
363 std::vector<uint256> carolNFTs = genPackedTokens(env, carol);
364 BEAST_EXPECT(nftCount(env, carol) == 96);
365 BEAST_EXPECT(ownerCount(env, carol) == 3);
366
367 // Get the index of the middle page.
368 uint256 const carolMiddleNFTokenPageIndex = [&env, &carol]() {
369 auto lastNFTokenPage = env.le(keylet::nftpage_max(carol));
370 return lastNFTokenPage->at(sfPreviousPageMin);
371 }();
372
373 // carol sells all of the tokens in the very last page to daria.
374 std::vector<uint256> dariaNFTs;
375 dariaNFTs.reserve(32);
376 for (int i = 0; i < 32; ++i)
377 {
378 uint256 const offerIndex =
379 keylet::nftoffer(carol, env.seq(carol)).key;
380 env(token::createOffer(carol, carolNFTs.back(), XRP(0)),
381 txflags(tfSellNFToken));
382 env.close();
383
384 env(token::acceptSellOffer(daria, offerIndex));
385 env.close();
386
387 dariaNFTs.push_back(carolNFTs.back());
388 carolNFTs.pop_back();
389 }
390 BEAST_EXPECT(nftCount(env, carol) == 64);
391 BEAST_EXPECT(ownerCount(env, carol) == 2);
392
393 // Removing the last token from the last page deletes the last
394 // page. This is a bug. The contents of the next-to-last page
395 // should have been moved into the last page.
396 BEAST_EXPECT(!env.le(keylet::nftpage_max(carol)));
397
398 // carol's "middle" page is still present, but has lost the
399 // NextPageMin field.
400 auto carolMiddleNFTokenPage = env.le(keylet::nftpage(
401 keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
402 if (!BEAST_EXPECT(carolMiddleNFTokenPage))
403 return;
404
405 BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
406 BEAST_EXPECT(!carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
407
408 // At this point carol's NFT directory has the same problem that
409 // bob's has: the last page is missing. Now we make things more
410 // complicated by putting the last page back. carol buys their NFTs
411 // back from daria.
412 for (uint256 const& nft : dariaNFTs)
413 {
414 uint256 const offerIndex =
415 keylet::nftoffer(carol, env.seq(carol)).key;
416 env(token::createOffer(carol, nft, drops(1)), token::owner(daria));
417 env.close();
418
419 env(token::acceptBuyOffer(daria, offerIndex));
420 env.close();
421
422 carolNFTs.push_back(nft);
423 }
424
425 // Note that carol actually owns 96 NFTs, but only 64 are reported
426 // because the links are damaged.
427 BEAST_EXPECT(nftCount(env, carol) == 64);
428 BEAST_EXPECT(ownerCount(env, carol) == 3);
429
430 // carol's "middle" page is present and still has no NextPageMin field.
431 {
432 auto carolMiddleNFTokenPage = env.le(keylet::nftpage(
433 keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
434 if (!BEAST_EXPECT(carolMiddleNFTokenPage))
435 return;
436
437 BEAST_EXPECT(
438 carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
439 BEAST_EXPECT(
440 !carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
441 }
442 // carol has a "last" page again, but it has no PreviousPageMin field.
443 {
444 auto carolLastNFTokenPage = env.le(keylet::nftpage_max(carol));
445
446 BEAST_EXPECT(
447 !carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
448 BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
449 }
450
451 //**********************************************************************
452 // Step 2: Enable the fixNFTokenPageLinks amendment.
453 //**********************************************************************
454 // Verify that the LedgerStateFix transaction is not enabled.
455 auto const linkFixFee = drops(env.current()->fees().increment);
456 env(ledgerStateFix::nftPageLinks(daria, alice),
457 fee(linkFixFee),
458 ter(temDISABLED));
459
460 // Wait 15 ledgers so the LedgerStateFix transaction is no longer
461 // retried.
462 for (int i = 0; i < 15; ++i)
463 env.close();
464
465 env.enableFeature(fixNFTokenPageLinks);
466 env.close();
467
468 //**********************************************************************
469 // Step 3A: Repair the one-page directory (alice's)
470 //**********************************************************************
471
472 // Verify that alice's NFToken directory is still damaged.
473
474 // alice's last page should still be missing.
475 BEAST_EXPECT(!env.le(keylet::nftpage_max(alice)));
476
477 // alice's "middle" page is still present and has no links.
478 {
479 auto aliceMiddleNFTokenPage = env.le(keylet::nftpage(
480 keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex));
481 if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
482 return;
483
484 BEAST_EXPECT(
485 !aliceMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
486 BEAST_EXPECT(
487 !aliceMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
488 }
489
490 // The server "remembers" daria's failed nftPageLinks transaction
491 // signature. So we need to advance daria's sequence number before
492 // daria can submit a similar transaction.
493 env(noop(daria));
494
495 // daria fixes the links in alice's NFToken directory.
496 env(ledgerStateFix::nftPageLinks(daria, alice), fee(linkFixFee));
497 env.close();
498
499 // alices's last page should now be present and include no links.
500 {
501 auto aliceLastNFTokenPage = env.le(keylet::nftpage_max(alice));
502 if (!BEAST_EXPECT(aliceLastNFTokenPage))
503 return;
504
505 BEAST_EXPECT(
506 !aliceLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
507 BEAST_EXPECT(!aliceLastNFTokenPage->isFieldPresent(sfNextPageMin));
508 }
509
510 // alice's middle page should be gone.
511 BEAST_EXPECT(!env.le(keylet::nftpage(
512 keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex)));
513
514 BEAST_EXPECT(nftCount(env, alice) == 32);
515 BEAST_EXPECT(ownerCount(env, alice) == 1);
516
517 //**********************************************************************
518 // Step 3B: Repair the two-page directory (bob's)
519 //**********************************************************************
520
521 // Verify that bob's NFToken directory is still damaged.
522
523 // bob's last page should still be missing.
524 BEAST_EXPECT(!env.le(keylet::nftpage_max(bob)));
525
526 // bob's "middle" page is still present and missing NextPageMin.
527 {
528 auto bobMiddleNFTokenPage = env.le(keylet::nftpage(
529 keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex));
530 if (!BEAST_EXPECT(bobMiddleNFTokenPage))
531 return;
532
533 BEAST_EXPECT(
534 bobMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
535 BEAST_EXPECT(!bobMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
536 }
537
538 // daria fixes the links in bob's NFToken directory.
539 env(ledgerStateFix::nftPageLinks(daria, bob), fee(linkFixFee));
540 env.close();
541
542 // bob's last page should now be present and include a previous
543 // link but no next link.
544 {
545 auto const lastPageKeylet = keylet::nftpage_max(bob);
546 auto const bobLastNFTokenPage = env.le(lastPageKeylet);
547 if (!BEAST_EXPECT(bobLastNFTokenPage))
548 return;
549
550 BEAST_EXPECT(bobLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
551 BEAST_EXPECT(
552 bobLastNFTokenPage->at(sfPreviousPageMin) !=
553 bobMiddleNFTokenPageIndex);
554 BEAST_EXPECT(!bobLastNFTokenPage->isFieldPresent(sfNextPageMin));
555
556 auto const bobNewFirstNFTokenPage = env.le(keylet::nftpage(
558 bobLastNFTokenPage->at(sfPreviousPageMin)));
559 if (!BEAST_EXPECT(bobNewFirstNFTokenPage))
560 return;
561
562 BEAST_EXPECT(
563 bobNewFirstNFTokenPage->isFieldPresent(sfNextPageMin) &&
564 bobNewFirstNFTokenPage->at(sfNextPageMin) ==
565 lastPageKeylet.key);
566 BEAST_EXPECT(
567 !bobNewFirstNFTokenPage->isFieldPresent(sfPreviousPageMin));
568 }
569
570 // bob's middle page should be gone.
571 BEAST_EXPECT(!env.le(keylet::nftpage(
572 keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex)));
573
574 BEAST_EXPECT(nftCount(env, bob) == 64);
575 BEAST_EXPECT(ownerCount(env, bob) == 2);
576
577 //**********************************************************************
578 // Step 3C: Repair the three-page directory (carol's)
579 //**********************************************************************
580
581 // Verify that carol's NFToken directory is still damaged.
582
583 // carol's "middle" page is present and has no NextPageMin field.
584 {
585 auto carolMiddleNFTokenPage = env.le(keylet::nftpage(
586 keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
587 if (!BEAST_EXPECT(carolMiddleNFTokenPage))
588 return;
589
590 BEAST_EXPECT(
591 carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
592 BEAST_EXPECT(
593 !carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
594 }
595 // carol has a "last" page, but it has no PreviousPageMin field.
596 {
597 auto carolLastNFTokenPage = env.le(keylet::nftpage_max(carol));
598
599 BEAST_EXPECT(
600 !carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
601 BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
602 }
603
604 // carol fixes the links in their own NFToken directory.
605 env(ledgerStateFix::nftPageLinks(carol, carol), fee(linkFixFee));
606 env.close();
607
608 {
609 // carol's "middle" page is present and now has a NextPageMin field.
610 auto const lastPageKeylet = keylet::nftpage_max(carol);
611 auto carolMiddleNFTokenPage = env.le(keylet::nftpage(
612 keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
613 if (!BEAST_EXPECT(carolMiddleNFTokenPage))
614 return;
615
616 BEAST_EXPECT(
617 carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
618 BEAST_EXPECT(
619 carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin) &&
620 carolMiddleNFTokenPage->at(sfNextPageMin) ==
621 lastPageKeylet.key);
622
623 // carol has a "last" page that includes a PreviousPageMin field.
624 auto carolLastNFTokenPage = env.le(lastPageKeylet);
625 if (!BEAST_EXPECT(carolLastNFTokenPage))
626 return;
627
628 BEAST_EXPECT(
629 carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin) &&
630 carolLastNFTokenPage->at(sfPreviousPageMin) ==
631 carolMiddleNFTokenPageIndex);
632 BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
633
634 // carol also has a "first" page that includes a NextPageMin field.
635 auto carolFirstNFTokenPage = env.le(keylet::nftpage(
636 keylet::nftpage_min(carol),
637 carolMiddleNFTokenPage->at(sfPreviousPageMin)));
638 if (!BEAST_EXPECT(carolFirstNFTokenPage))
639 return;
640
641 BEAST_EXPECT(
642 carolFirstNFTokenPage->isFieldPresent(sfNextPageMin) &&
643 carolFirstNFTokenPage->at(sfNextPageMin) ==
644 carolMiddleNFTokenPageIndex);
645 BEAST_EXPECT(
646 !carolFirstNFTokenPage->isFieldPresent(sfPreviousPageMin));
647 }
648
649 // With the link repair, the server knows that carol has 96 NFTs.
650 BEAST_EXPECT(nftCount(env, carol) == 96);
651 BEAST_EXPECT(ownerCount(env, carol) == 3);
652 }
653
654public:
655 void
662};
663
664BEAST_DEFINE_TESTSUITE(FixNFTokenPageLinks, app, ripple);
665
666} // 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.
Value removeMember(char const *key)
Remove and return the named member.
A testsuite class.
Definition suite.h:55
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:155
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
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:278
T end(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
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
@ telINSUF_FEE_P
Definition TER.h:57
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:230
constexpr std::uint32_t tfPassive
Definition TxFlags.h:98
@ tefINVALID_LEDGER_FIX_TYPE
Definition TER.h:187
@ tecOBJECT_NOT_FOUND
Definition TER.h:327
@ tecFAILED_PROCESSING
Definition TER.h:287
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:630
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:142
@ temINVALID
Definition TER.h:110
@ temINVALID_FLAG
Definition TER.h:111
@ temDISABLED
Definition TER.h:114
T pop_back(T... args)
T push_back(T... args)
T reserve(T... args)
T sort(T... args)
uint256 key
Definition Keylet.h:40