mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
This change updates the ColumnLimit from 80 to 120, and applies clang-format to reformat the code.
594 lines
24 KiB
C++
594 lines
24 KiB
C++
#include <test/jtx.h>
|
|
|
|
#include <xrpld/app/tx/detail/ApplyContext.h>
|
|
#include <xrpld/app/tx/detail/NFTokenUtils.h>
|
|
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
namespace xrpl {
|
|
|
|
class FixNFTokenPageLinks_test : public beast::unit_test::suite
|
|
{
|
|
// Helper function that returns the number of nfts owned by an account.
|
|
static std::uint32_t
|
|
nftCount(test::jtx::Env& env, test::jtx::Account const& acct)
|
|
{
|
|
Json::Value params;
|
|
params[jss::account] = acct.human();
|
|
params[jss::type] = "state";
|
|
Json::Value nfts = env.rpc("json", "account_nfts", to_string(params));
|
|
return nfts[jss::result][jss::account_nfts].size();
|
|
};
|
|
|
|
// A helper function that generates 96 nfts packed into three pages
|
|
// of 32 each. Returns a sorted vector of the NFTokenIDs packed into
|
|
// the pages.
|
|
std::vector<uint256>
|
|
genPackedTokens(test::jtx::Env& env, test::jtx::Account const& owner)
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
std::vector<uint256> nfts;
|
|
nfts.reserve(96);
|
|
|
|
// We want to create fully packed NFT pages. This is a little
|
|
// tricky since the system currently in place is inclined to
|
|
// assign consecutive tokens to only 16 entries per page.
|
|
//
|
|
// By manipulating the internal form of the taxon we can force
|
|
// creation of NFT pages that are completely full. This lambda
|
|
// tells us the taxon value we should pass in in order for the
|
|
// internal representation to match the passed in value.
|
|
auto internalTaxon = [this, &env](Account const& acct, std::uint32_t taxon) -> std::uint32_t {
|
|
std::uint32_t tokenSeq = [this, &env, &acct]() {
|
|
auto const le = env.le(acct);
|
|
if (BEAST_EXPECT(le))
|
|
return le->at(~sfMintedNFTokens).value_or(0u);
|
|
return 0u;
|
|
}();
|
|
|
|
// We must add FirstNFTokenSequence.
|
|
tokenSeq += env.le(acct)->at(~sfFirstNFTokenSequence).value_or(env.seq(acct));
|
|
|
|
return toUInt32(nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
|
|
};
|
|
|
|
for (std::uint32_t i = 0; i < 96; ++i)
|
|
{
|
|
// In order to fill the pages we use the taxon to break them
|
|
// into groups of 16 entries. By having the internal
|
|
// representation of the taxon go...
|
|
// 0, 3, 2, 5, 4, 7...
|
|
// in sets of 16 NFTs we can get each page to be fully
|
|
// populated.
|
|
std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
|
|
uint32_t const extTaxon = internalTaxon(owner, intTaxon);
|
|
nfts.push_back(token::getNextID(env, owner, extTaxon, tfTransferable));
|
|
env(token::mint(owner, extTaxon), txflags(tfTransferable));
|
|
env.close();
|
|
}
|
|
|
|
// Sort the NFTs so they are listed in storage order, not
|
|
// creation order.
|
|
std::sort(nfts.begin(), nfts.end());
|
|
|
|
// Verify that the owner does indeed have exactly three pages
|
|
// of NFTs with 32 entries in each page.
|
|
{
|
|
Json::Value params;
|
|
params[jss::account] = owner.human();
|
|
auto resp = env.rpc("json", "account_objects", to_string(params));
|
|
|
|
Json::Value const& acctObjs = resp[jss::result][jss::account_objects];
|
|
|
|
int pageCount = 0;
|
|
for (Json::UInt i = 0; i < acctObjs.size(); ++i)
|
|
{
|
|
if (BEAST_EXPECT(
|
|
acctObjs[i].isMember(sfNFTokens.jsonName) && acctObjs[i][sfNFTokens.jsonName].isArray()))
|
|
{
|
|
BEAST_EXPECT(acctObjs[i][sfNFTokens.jsonName].size() == 32);
|
|
++pageCount;
|
|
}
|
|
}
|
|
// If this check fails then the internal NFT directory logic
|
|
// has changed.
|
|
BEAST_EXPECT(pageCount == 3);
|
|
}
|
|
return nfts;
|
|
};
|
|
|
|
void
|
|
testLedgerStateFixErrors()
|
|
{
|
|
testcase("LedgerStateFix error cases");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
|
|
{
|
|
// Verify that the LedgerStateFix transaction is disabled
|
|
// without the fixNFTokenPageLinks amendment.
|
|
Env env{*this, testable_amendments() - fixNFTokenPageLinks};
|
|
env.fund(XRP(1000), alice);
|
|
|
|
auto const linkFixFee = drops(env.current()->fees().increment);
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), fee(linkFixFee), ter(temDISABLED));
|
|
}
|
|
|
|
Env env{*this, testable_amendments()};
|
|
env.fund(XRP(1000), alice);
|
|
std::uint32_t const ticketSeq = env.seq(alice);
|
|
env(ticket::create(alice, 1));
|
|
|
|
// Preflight
|
|
|
|
{
|
|
// Fail preflight1. Can't combine AccountTxnID and ticket.
|
|
Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
|
|
tx[sfAccountTxnID.jsonName] =
|
|
"00000000000000000000000000000000"
|
|
"00000000000000000000000000000000";
|
|
env(tx, ticket::use(ticketSeq), ter(temINVALID));
|
|
}
|
|
// Fee too low.
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), ter(telINSUF_FEE_P));
|
|
|
|
// Invalid flags.
|
|
auto const linkFixFee = drops(env.current()->fees().increment);
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), fee(linkFixFee), txflags(tfPassive), ter(temINVALID_FLAG));
|
|
|
|
{
|
|
// ledgerStateFix::nftPageLinks requires an Owner field.
|
|
Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
|
|
tx.removeMember(sfOwner.jsonName);
|
|
env(tx, fee(linkFixFee), ter(temINVALID));
|
|
}
|
|
{
|
|
// Invalid LedgerFixType codes.
|
|
Json::Value tx = ledgerStateFix::nftPageLinks(alice, alice);
|
|
tx[sfLedgerFixType.jsonName] = 0;
|
|
env(tx, fee(linkFixFee), ter(tefINVALID_LEDGER_FIX_TYPE));
|
|
|
|
tx[sfLedgerFixType.jsonName] = 200;
|
|
env(tx, fee(linkFixFee), ter(tefINVALID_LEDGER_FIX_TYPE));
|
|
}
|
|
|
|
// Preclaim
|
|
Account const carol("carol");
|
|
env.memoize(carol);
|
|
env(ledgerStateFix::nftPageLinks(alice, carol), fee(linkFixFee), ter(tecOBJECT_NOT_FOUND));
|
|
}
|
|
|
|
void
|
|
testTokenPageLinkErrors()
|
|
{
|
|
testcase("NFTokenPageLinkFix error cases");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
|
|
Env env{*this, testable_amendments()};
|
|
env.fund(XRP(1000), alice);
|
|
|
|
// These cases all return the same TER code, but they exercise
|
|
// different cases where there is nothing to fix in an owner's
|
|
// NFToken pages. So they increase test coverage.
|
|
|
|
// Owner has no pages to fix.
|
|
auto const linkFixFee = drops(env.current()->fees().increment);
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), fee(linkFixFee), ter(tecFAILED_PROCESSING));
|
|
|
|
// Alice has only one page.
|
|
env(token::mint(alice), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), fee(linkFixFee), ter(tecFAILED_PROCESSING));
|
|
|
|
// Alice has at least three pages.
|
|
for (std::uint32_t i = 0; i < 64; ++i)
|
|
{
|
|
env(token::mint(alice), txflags(tfTransferable));
|
|
env.close();
|
|
}
|
|
|
|
env(ledgerStateFix::nftPageLinks(alice, alice), fee(linkFixFee), ter(tecFAILED_PROCESSING));
|
|
}
|
|
|
|
void
|
|
testFixNFTokenPageLinks()
|
|
{
|
|
// Steps:
|
|
// 1. Before the fixNFTokenPageLinks amendment is enabled, build the
|
|
// three kinds of damaged NFToken directories we know about:
|
|
// A. One where there is only one page, but without the final index.
|
|
// B. One with multiple pages and a missing final page.
|
|
// C. One with links missing in the middle of the chain.
|
|
// 2. Enable the fixNFTokenPageLinks amendment.
|
|
// 3. Invoke the LedgerStateFix transactor and repair the directories.
|
|
testcase("Fix links");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const daria("daria");
|
|
|
|
Env env{*this, testable_amendments() - fixNFTokenPageLinks};
|
|
env.fund(XRP(1000), alice, bob, carol, daria);
|
|
|
|
//**********************************************************************
|
|
// Step 1A: Create damaged NFToken directories:
|
|
// o One where there is only one page, but without the final index.
|
|
//**********************************************************************
|
|
|
|
// alice generates three packed pages.
|
|
std::vector<uint256> aliceNFTs = genPackedTokens(env, alice);
|
|
BEAST_EXPECT(nftCount(env, alice) == 96);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
|
|
// Get the index of the middle page.
|
|
uint256 const aliceMiddleNFTokenPageIndex = [&env, &alice]() {
|
|
auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
|
|
return lastNFTokenPage->at(sfPreviousPageMin);
|
|
}();
|
|
|
|
// alice burns all the tokens in the first and last pages.
|
|
for (int i = 0; i < 32; ++i)
|
|
{
|
|
env(token::burn(alice, {aliceNFTs[i]}));
|
|
env.close();
|
|
}
|
|
aliceNFTs.erase(aliceNFTs.begin(), aliceNFTs.begin() + 32);
|
|
for (int i = 0; i < 32; ++i)
|
|
{
|
|
env(token::burn(alice, {aliceNFTs.back()}));
|
|
aliceNFTs.pop_back();
|
|
env.close();
|
|
}
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(nftCount(env, alice) == 32);
|
|
|
|
// Removing the last token from the last page deletes the last
|
|
// page. This is a bug. The contents of the next-to-last page
|
|
// should have been moved into the last page.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage_max(alice)));
|
|
|
|
// alice's "middle" page is still present, but has no links.
|
|
{
|
|
auto aliceMiddleNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(!aliceMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!aliceMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
//**********************************************************************
|
|
// Step 1B: Create damaged NFToken directories:
|
|
// o One with multiple pages and a missing final page.
|
|
//**********************************************************************
|
|
|
|
// bob generates three packed pages.
|
|
std::vector<uint256> bobNFTs = genPackedTokens(env, bob);
|
|
BEAST_EXPECT(nftCount(env, bob) == 96);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 3);
|
|
|
|
// Get the index of the middle page.
|
|
uint256 const bobMiddleNFTokenPageIndex = [&env, &bob]() {
|
|
auto lastNFTokenPage = env.le(keylet::nftpage_max(bob));
|
|
return lastNFTokenPage->at(sfPreviousPageMin);
|
|
}();
|
|
|
|
// bob burns all the tokens in the very last page.
|
|
for (int i = 0; i < 32; ++i)
|
|
{
|
|
env(token::burn(bob, {bobNFTs.back()}));
|
|
bobNFTs.pop_back();
|
|
env.close();
|
|
}
|
|
BEAST_EXPECT(nftCount(env, bob) == 64);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 2);
|
|
|
|
// Removing the last token from the last page deletes the last
|
|
// page. This is a bug. The contents of the next-to-last page
|
|
// should have been moved into the last page.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage_max(bob)));
|
|
|
|
// bob's "middle" page is still present, but has lost the
|
|
// NextPageMin field.
|
|
{
|
|
auto bobMiddleNFTokenPage = env.le(keylet::nftpage(keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(bobMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(bobMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!bobMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
//**********************************************************************
|
|
// Step 1C: Create damaged NFToken directories:
|
|
// o One with links missing in the middle of the chain.
|
|
//**********************************************************************
|
|
|
|
// carol generates three packed pages.
|
|
std::vector<uint256> carolNFTs = genPackedTokens(env, carol);
|
|
BEAST_EXPECT(nftCount(env, carol) == 96);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 3);
|
|
|
|
// Get the index of the middle page.
|
|
uint256 const carolMiddleNFTokenPageIndex = [&env, &carol]() {
|
|
auto lastNFTokenPage = env.le(keylet::nftpage_max(carol));
|
|
return lastNFTokenPage->at(sfPreviousPageMin);
|
|
}();
|
|
|
|
// carol sells all of the tokens in the very last page to daria.
|
|
std::vector<uint256> dariaNFTs;
|
|
dariaNFTs.reserve(32);
|
|
for (int i = 0; i < 32; ++i)
|
|
{
|
|
uint256 const offerIndex = keylet::nftoffer(carol, env.seq(carol)).key;
|
|
env(token::createOffer(carol, carolNFTs.back(), XRP(0)), txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
env(token::acceptSellOffer(daria, offerIndex));
|
|
env.close();
|
|
|
|
dariaNFTs.push_back(carolNFTs.back());
|
|
carolNFTs.pop_back();
|
|
}
|
|
BEAST_EXPECT(nftCount(env, carol) == 64);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 2);
|
|
|
|
// Removing the last token from the last page deletes the last
|
|
// page. This is a bug. The contents of the next-to-last page
|
|
// should have been moved into the last page.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage_max(carol)));
|
|
|
|
// carol's "middle" page is still present, but has lost the
|
|
// NextPageMin field.
|
|
auto carolMiddleNFTokenPage = env.le(keylet::nftpage(keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(carolMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
|
|
// At this point carol's NFT directory has the same problem that
|
|
// bob's has: the last page is missing. Now we make things more
|
|
// complicated by putting the last page back. carol buys their NFTs
|
|
// back from daria.
|
|
for (uint256 const& nft : dariaNFTs)
|
|
{
|
|
uint256 const offerIndex = keylet::nftoffer(carol, env.seq(carol)).key;
|
|
env(token::createOffer(carol, nft, drops(1)), token::owner(daria));
|
|
env.close();
|
|
|
|
env(token::acceptBuyOffer(daria, offerIndex));
|
|
env.close();
|
|
|
|
carolNFTs.push_back(nft);
|
|
}
|
|
|
|
// Note that carol actually owns 96 NFTs, but only 64 are reported
|
|
// because the links are damaged.
|
|
BEAST_EXPECT(nftCount(env, carol) == 64);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 3);
|
|
|
|
// carol's "middle" page is present and still has no NextPageMin field.
|
|
{
|
|
auto carolMiddleNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(carolMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
// carol has a "last" page again, but it has no PreviousPageMin field.
|
|
{
|
|
auto carolLastNFTokenPage = env.le(keylet::nftpage_max(carol));
|
|
|
|
BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
//**********************************************************************
|
|
// Step 2: Enable the fixNFTokenPageLinks amendment.
|
|
//**********************************************************************
|
|
// Verify that the LedgerStateFix transaction is not enabled.
|
|
auto const linkFixFee = drops(env.current()->fees().increment);
|
|
env(ledgerStateFix::nftPageLinks(daria, alice), fee(linkFixFee), ter(temDISABLED));
|
|
|
|
// Wait 15 ledgers so the LedgerStateFix transaction is no longer
|
|
// retried.
|
|
for (int i = 0; i < 15; ++i)
|
|
env.close();
|
|
|
|
env.enableFeature(fixNFTokenPageLinks);
|
|
env.close();
|
|
|
|
//**********************************************************************
|
|
// Step 3A: Repair the one-page directory (alice's)
|
|
//**********************************************************************
|
|
|
|
// Verify that alice's NFToken directory is still damaged.
|
|
|
|
// alice's last page should still be missing.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage_max(alice)));
|
|
|
|
// alice's "middle" page is still present and has no links.
|
|
{
|
|
auto aliceMiddleNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(!aliceMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!aliceMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
// The server "remembers" daria's failed nftPageLinks transaction
|
|
// signature. So we need to advance daria's sequence number before
|
|
// daria can submit a similar transaction.
|
|
env(noop(daria));
|
|
|
|
// daria fixes the links in alice's NFToken directory.
|
|
env(ledgerStateFix::nftPageLinks(daria, alice), fee(linkFixFee));
|
|
env.close();
|
|
|
|
// alices's last page should now be present and include no links.
|
|
{
|
|
auto aliceLastNFTokenPage = env.le(keylet::nftpage_max(alice));
|
|
if (!BEAST_EXPECT(aliceLastNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(!aliceLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!aliceLastNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
// alice's middle page should be gone.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage(keylet::nftpage_min(alice), aliceMiddleNFTokenPageIndex)));
|
|
|
|
BEAST_EXPECT(nftCount(env, alice) == 32);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
//**********************************************************************
|
|
// Step 3B: Repair the two-page directory (bob's)
|
|
//**********************************************************************
|
|
|
|
// Verify that bob's NFToken directory is still damaged.
|
|
|
|
// bob's last page should still be missing.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage_max(bob)));
|
|
|
|
// bob's "middle" page is still present and missing NextPageMin.
|
|
{
|
|
auto bobMiddleNFTokenPage = env.le(keylet::nftpage(keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(bobMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(bobMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!bobMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
// daria fixes the links in bob's NFToken directory.
|
|
env(ledgerStateFix::nftPageLinks(daria, bob), fee(linkFixFee));
|
|
env.close();
|
|
|
|
// bob's last page should now be present and include a previous
|
|
// link but no next link.
|
|
{
|
|
auto const lastPageKeylet = keylet::nftpage_max(bob);
|
|
auto const bobLastNFTokenPage = env.le(lastPageKeylet);
|
|
if (!BEAST_EXPECT(bobLastNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(bobLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(bobLastNFTokenPage->at(sfPreviousPageMin) != bobMiddleNFTokenPageIndex);
|
|
BEAST_EXPECT(!bobLastNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
|
|
auto const bobNewFirstNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(bob), bobLastNFTokenPage->at(sfPreviousPageMin)));
|
|
if (!BEAST_EXPECT(bobNewFirstNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(
|
|
bobNewFirstNFTokenPage->isFieldPresent(sfNextPageMin) &&
|
|
bobNewFirstNFTokenPage->at(sfNextPageMin) == lastPageKeylet.key);
|
|
BEAST_EXPECT(!bobNewFirstNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
}
|
|
|
|
// bob's middle page should be gone.
|
|
BEAST_EXPECT(!env.le(keylet::nftpage(keylet::nftpage_min(bob), bobMiddleNFTokenPageIndex)));
|
|
|
|
BEAST_EXPECT(nftCount(env, bob) == 64);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 2);
|
|
|
|
//**********************************************************************
|
|
// Step 3C: Repair the three-page directory (carol's)
|
|
//**********************************************************************
|
|
|
|
// Verify that carol's NFToken directory is still damaged.
|
|
|
|
// carol's "middle" page is present and has no NextPageMin field.
|
|
{
|
|
auto carolMiddleNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(carolMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
// carol has a "last" page, but it has no PreviousPageMin field.
|
|
{
|
|
auto carolLastNFTokenPage = env.le(keylet::nftpage_max(carol));
|
|
|
|
BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
}
|
|
|
|
// carol fixes the links in their own NFToken directory.
|
|
env(ledgerStateFix::nftPageLinks(carol, carol), fee(linkFixFee));
|
|
env.close();
|
|
|
|
{
|
|
// carol's "middle" page is present and now has a NextPageMin field.
|
|
auto const lastPageKeylet = keylet::nftpage_max(carol);
|
|
auto carolMiddleNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(carol), carolMiddleNFTokenPageIndex));
|
|
if (!BEAST_EXPECT(carolMiddleNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
BEAST_EXPECT(
|
|
carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin) &&
|
|
carolMiddleNFTokenPage->at(sfNextPageMin) == lastPageKeylet.key);
|
|
|
|
// carol has a "last" page that includes a PreviousPageMin field.
|
|
auto carolLastNFTokenPage = env.le(lastPageKeylet);
|
|
if (!BEAST_EXPECT(carolLastNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(
|
|
carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin) &&
|
|
carolLastNFTokenPage->at(sfPreviousPageMin) == carolMiddleNFTokenPageIndex);
|
|
BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
|
|
|
|
// carol also has a "first" page that includes a NextPageMin field.
|
|
auto carolFirstNFTokenPage =
|
|
env.le(keylet::nftpage(keylet::nftpage_min(carol), carolMiddleNFTokenPage->at(sfPreviousPageMin)));
|
|
if (!BEAST_EXPECT(carolFirstNFTokenPage))
|
|
return;
|
|
|
|
BEAST_EXPECT(
|
|
carolFirstNFTokenPage->isFieldPresent(sfNextPageMin) &&
|
|
carolFirstNFTokenPage->at(sfNextPageMin) == carolMiddleNFTokenPageIndex);
|
|
BEAST_EXPECT(!carolFirstNFTokenPage->isFieldPresent(sfPreviousPageMin));
|
|
}
|
|
|
|
// With the link repair, the server knows that carol has 96 NFTs.
|
|
BEAST_EXPECT(nftCount(env, carol) == 96);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 3);
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testLedgerStateFixErrors();
|
|
testTokenPageLinkErrors();
|
|
testFixNFTokenPageLinks();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(FixNFTokenPageLinks, app, xrpl);
|
|
|
|
} // namespace xrpl
|