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