Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix "account_nfts" with unassociated marker returning issue #5045

Merged
merged 16 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/ripple/rpc/handlers/AccountObjects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ doAccountNFTs(RPC::JsonContext& context)
return *err;

uint256 marker;
bool markerSet = false;

if (params.isMember(jss::marker))
{
markerSet = true;
auto const& m = params[jss::marker];
if (!m.isString())
return RPC::expected_field_error(jss::marker, "string");
Expand All @@ -98,6 +100,7 @@ doAccountNFTs(RPC::JsonContext& context)

// Continue iteration from the current page:
bool pastMarker = marker.isZero();
bool markerFound = false;
uint256 const maskedMarker = marker & nft::pageMask;
while (cp)
{
Expand All @@ -123,9 +126,18 @@ doAccountNFTs(RPC::JsonContext& context)
continue;

if (!pastMarker && maskedNftokenID == maskedMarker &&
nftokenID <= marker)
nftokenID < marker)
continue;

if (nftokenID == marker)
{
markerFound = true;
continue;
}

if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);

pastMarker = true;

{
Expand Down Expand Up @@ -155,6 +167,9 @@ doAccountNFTs(RPC::JsonContext& context)
cp = nullptr;
}

if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);

yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
result[jss::account] = toBase58(accountID);
context.loadType = Resource::feeMediumBurdenRPC;
return result;
Expand Down
121 changes: 121 additions & 0 deletions src/test/rpc/AccountObjects_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1032,13 +1032,134 @@ class AccountObjects_test : public beast::unit_test::suite
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::hashes), 0));
}

void
testNFTsMarker()
{
testcase("NFTsMarker");
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved

using namespace jtx;
Env env(*this);

Account const bob{"bob"};
env.fund(XRP(10000), bob);

yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
unsigned nftsSize = 10;
for (unsigned i = 0; i < nftsSize; i++)
{
env(token::mint(bob, 0));
}

env.close();

auto createFakeMarker = [](AccountID const& issuer,
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
std::string taxonStr,
std::string tokenSeqStr,
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
std::uint16_t flags = 0,
std::uint16_t fee = 0) {
flags = boost::endian::native_to_big(flags);
fee = boost::endian::native_to_big(fee);
std::array<std::uint8_t, 32> buf{};
auto ptr = buf.data();
std::memcpy(ptr, &flags, sizeof(flags));
ptr += sizeof(flags);
std::memcpy(ptr, &fee, sizeof(fee));
ptr += sizeof(fee);
std::memcpy(ptr, issuer.data(), issuer.size());
ptr += issuer.size();
auto taxon =
static_cast<uint32_t>(std::stoul(taxonStr, nullptr, 16));
std::memcpy(ptr, &taxon, sizeof(taxon));
ptr += sizeof(taxon);
auto tokenSeq =
static_cast<uint32_t>(std::stoul(tokenSeqStr, nullptr, 16));
std::memcpy(ptr, &tokenSeq, sizeof(tokenSeq));
ptr += sizeof(tokenSeq);
return uint256::fromVoid(buf.data());
};

// save the NFTokenIDs to use later
std::vector<Json::Value> tokenIDs;
{
Json::Value params;
params[jss::account] = bob.human();
params[jss::ledger_index] = "validated";
auto const resp =
env.rpc("json", "account_nfts", to_string(params));
auto const& nfts = resp[jss::result][jss::account_nfts];
for (auto const& nft : nfts)
tokenIDs.push_back(nft["NFTokenID"]);
}

auto compareNFTs = [&](unsigned const limit, unsigned const lastIndex) {
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
Json::Value params;
params[jss::account] = bob.human();
params[jss::limit] = limit;
params[jss::marker] = tokenIDs[lastIndex];
params[jss::ledger_index] = "validated";
auto const resp =
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
env.rpc("json", "account_nfts", to_string(params));

if (resp[jss::result].isMember(jss::error))
return false;

auto const& nfts = resp[jss::result][jss::account_nfts];
auto nftsCount = nftsSize - lastIndex - 1 < limit ? nftsSize - lastIndex - 1: limit;

if (nfts.size() != nftsCount)
return false;

for (unsigned i = 0; i < nftsCount; i++)
{
if (nfts[i]["NFTokenID"] != tokenIDs[lastIndex + 1 + i])
return false;
}

return true;
};

// test a valid marker which is equal to the third tokenID
BEAST_EXPECT(compareNFTs(4, 2));

// test a valid marker which is equal to the 8th tokenID
BEAST_EXPECT(compareNFTs(4, 7));

// test an invalid marker which does not exist in the NFTokenIDs
{
Json::Value params;
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
params[jss::account] = bob.human();
params[jss::limit] = 4;
params[jss::ledger_index] = "validated";
auto const marker =
createFakeMarker(bob.id(), "00000000", "00000000");
params[jss::marker] = to_string(marker);
auto const resp =
env.rpc("json", "account_nfts", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field \'marker\'.");
}

// test an invalid marker which exceeds the maximum value of the existing NFTokenID
{
Json::Value params;
params[jss::account] = bob.human();
params[jss::limit] = 4;
params[jss::ledger_index] = "validated";
auto const marker =
createFakeMarker(bob.id(), "FFFFFFFF", "FFFFFFFF");
params[jss::marker] = to_string(marker);
auto const resp =
env.rpc("json", "account_nfts", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field \'marker\'.");
}
}

void
run() override
{
testErrors();
testUnsteppedThenStepped();
testUnsteppedThenSteppedWithNFTs();
testObjectTypes();
testNFTsMarker();
}
};

Expand Down