nfts_by_issuer (#948)

Fixes issue #385

Original PR:
#584
This commit is contained in:
Shawn Xie
2023-10-30 15:53:32 -04:00
committed by GitHub
parent b363cc93af
commit 243858df12
15 changed files with 1196 additions and 4 deletions

View File

@@ -126,6 +126,7 @@ target_sources (clio PRIVATE
src/rpc/handlers/LedgerData.cpp
src/rpc/handlers/LedgerEntry.cpp
src/rpc/handlers/LedgerRange.cpp
src/rpc/handlers/NFTsByIssuer.cpp
src/rpc/handlers/NFTBuyOffers.cpp
src/rpc/handlers/NFTHistory.cpp
src/rpc/handlers/NFTInfo.cpp
@@ -205,6 +206,7 @@ if (tests)
unittests/rpc/handlers/RandomTests.cpp
unittests/rpc/handlers/NFTInfoTests.cpp
unittests/rpc/handlers/NFTBuyOffersTests.cpp
unittests/rpc/handlers/NFTsByIssuerTest.cpp
unittests/rpc/handlers/NFTSellOffersTests.cpp
unittests/rpc/handlers/NFTHistoryTests.cpp
unittests/rpc/handlers/SubscribeTests.cpp

View File

@@ -290,6 +290,28 @@ public:
boost::asio::yield_context yield
) const = 0;
/**
* @brief Fetches all NFTs issued by a given address.
*
* @param issuer AccountID of issuer you wish you query.
* @param taxon Optional taxon of NFTs by which you wish to filter.
* @param limit Paging limit.
* @param cursorIn Optional cursor to allow us to pick up from where we
* last left off.
* @param yield Currently executing coroutine.
* @return std::vector<NFT> of NFTs issued by this account, or
* this issuer/taxon combination if taxon is passed and an optional marker
*/
virtual NFTsAndCursor
fetchNFTsByIssuer(
ripple::AccountID const& issuer,
std::optional<std::uint32_t> const& taxon,
std::uint32_t ledgerSequence,
std::uint32_t limit,
std::optional<ripple::uint256> const& cursorIn,
boost::asio::yield_context yield
) const = 0;
/**
* @brief Fetches a specific ledger object.
*

View File

@@ -441,6 +441,96 @@ public:
return {txns, {}};
}
NFTsAndCursor
fetchNFTsByIssuer(
ripple::AccountID const& issuer,
std::optional<std::uint32_t> const& taxon,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursorIn,
boost::asio::yield_context yield
) const override
{
NFTsAndCursor ret;
Statement const idQueryStatement = [&taxon, &issuer, &cursorIn, &limit, this]() {
if (taxon.has_value()) {
auto r = schema_->selectNFTIDsByIssuerTaxon.bind(issuer);
r.bindAt(1, *taxon);
r.bindAt(2, cursorIn.value_or(ripple::uint256(0)));
r.bindAt(3, Limit{limit});
return r;
}
auto r = schema_->selectNFTIDsByIssuer.bind(issuer);
r.bindAt(
1,
std::make_tuple(
cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0,
cursorIn.value_or(ripple::uint256(0))
)
);
r.bindAt(2, Limit{limit});
return r;
}();
// Query for all the NFTs issued by the account, potentially filtered by the taxon
auto const res = executor_.read(yield, idQueryStatement);
auto const& idQueryResults = res.value();
if (not idQueryResults.hasRows()) {
LOG(log_.debug()) << "No rows returned";
return {};
}
std::vector<ripple::uint256> nftIDs;
for (auto const [nftID] : extract<ripple::uint256>(idQueryResults))
nftIDs.push_back(nftID);
if (nftIDs.empty())
return ret;
if (nftIDs.size() == limit)
ret.cursor = nftIDs.back();
auto const nftQueryStatement = schema_->selectNFTBulk.bind(nftIDs);
nftQueryStatement.bindAt(1, ledgerSequence);
// Fetch all the NFT data, meanwhile filtering out the NFTs that are not within the ledger range
auto const nftRes = executor_.read(yield, nftQueryStatement);
auto const& nftQueryResults = nftRes.value();
if (not nftQueryResults.hasRows()) {
LOG(log_.debug()) << "No rows returned";
return {};
}
auto const nftURIQueryStatement = schema_->selectNFTURIBulk.bind(nftIDs);
nftURIQueryStatement.bindAt(1, ledgerSequence);
// Get the URI for each NFT, but it's possible that URI doesn't exist
auto const uriRes = executor_.read(yield, nftURIQueryStatement);
auto const& nftURIQueryResults = uriRes.value();
std::unordered_map<std::string, Blob> nftURIMap;
for (auto const [nftID, uri] : extract<ripple::uint256, Blob>(nftURIQueryResults))
nftURIMap.insert({ripple::strHex(nftID), uri});
for (auto const [nftID, seq, owner, isBurned] :
extract<ripple::uint256, std::uint32_t, ripple::AccountID, bool>(nftQueryResults)) {
NFT nft;
nft.tokenID = nftID;
nft.ledgerSequence = seq;
nft.owner = owner;
nft.isBurned = isBurned;
if (nftURIMap.contains(ripple::strHex(nft.tokenID)))
nft.uri = nftURIMap.at(ripple::strHex(nft.tokenID));
ret.nfts.push_back(nft);
}
return ret;
}
std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const override

View File

@@ -162,6 +162,11 @@ struct NFT {
}
};
struct NFTsAndCursor {
std::vector<NFT> nfts;
std::optional<ripple::uint256> cursor;
};
/**
* @brief Stores a range of sequences as a min and max pair.
*/

View File

@@ -592,6 +592,20 @@ public:
));
}();
PreparedStatement selectNFTBulk = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT token_id, sequence, owner, is_burned
FROM {}
WHERE token_id IN ?
AND sequence <= ?
ORDER BY sequence DESC
PER PARTITION LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "nf_tokens")
));
}();
PreparedStatement selectNFTURI = [this]() {
return handle_.get().prepare(fmt::format(
R"(
@@ -606,6 +620,20 @@ public:
));
}();
PreparedStatement selectNFTURIBulk = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT token_id, uri
FROM {}
WHERE token_id IN ?
AND sequence <= ?
ORDER BY sequence DESC
PER PARTITION LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_uris")
));
}();
PreparedStatement selectNFTTx = [this]() {
return handle_.get().prepare(fmt::format(
R"(
@@ -634,6 +662,35 @@ public:
));
}();
PreparedStatement selectNFTIDsByIssuer = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT token_id
FROM {}
WHERE issuer = ?
AND (taxon, token_id) > ?
ORDER BY taxon ASC, token_id ASC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2")
));
}();
PreparedStatement selectNFTIDsByIssuerTaxon = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT token_id
FROM {}
WHERE issuer = ?
AND taxon = ?
AND token_id > ?
ORDER BY taxon ASC, token_id ASC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2")
));
}();
PreparedStatement selectLedgerByHash = [this]() {
return handle_.get().prepare(fmt::format(
R"(

View File

@@ -0,0 +1,87 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <ripple/basics/base_uint.h>
#include <cassandra.h>
#include <string>
#include <string_view>
namespace data::cassandra::detail {
class Collection : public ManagedObject<CassCollection> {
static constexpr auto deleter = [](CassCollection* ptr) { cass_collection_free(ptr); };
static void
throwErrorIfNeeded(CassError const rc, std::string_view const label)
{
if (rc == CASS_OK)
return;
auto const tag = '[' + std::string{label} + ']';
throw std::logic_error(tag + ": " + cass_error_desc(rc));
}
public:
/* implicit */ Collection(CassCollection* ptr);
template <typename Type>
explicit Collection(std::vector<Type> const& value)
: ManagedObject{cass_collection_new(CASS_COLLECTION_TYPE_LIST, value.size()), deleter}
{
bind(value);
}
template <typename Type>
void
bind(std::vector<Type> const& values) const
{
for (auto const& value : values)
append(value);
}
void
append(bool const value) const
{
auto const rc = cass_collection_append_bool(*this, value ? cass_true : cass_false);
throwErrorIfNeeded(rc, "Bind bool");
}
void
append(int64_t const value) const
{
auto const rc = cass_collection_append_int64(*this, value);
throwErrorIfNeeded(rc, "Bind int64");
}
void
append(ripple::uint256 const& value) const
{
auto const rc = cass_collection_append_bytes(
*this,
static_cast<cass_byte_t const*>(static_cast<unsigned char const*>(value.data())),
ripple::uint256::size()
);
throwErrorIfNeeded(rc, "Bind ripple::uint256");
}
};
} // namespace data::cassandra::detail

View File

@@ -20,6 +20,7 @@
#pragma once
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/Collection.h>
#include <data/cassandra/impl/ManagedObject.h>
#include <data/cassandra/impl/Tuple.h>
#include <util/Expected.h>
@@ -99,6 +100,8 @@ public:
using DecayedType = std::decay_t<Type>;
using UCharVectorType = std::vector<unsigned char>;
using UintTupleType = std::tuple<uint32_t, uint32_t>;
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
using ByteVectorType = std::vector<ripple::uint256>;
if constexpr (std::is_same_v<DecayedType, ripple::uint256>) {
auto const rc = bindBytes(value.data(), value.size());
@@ -113,9 +116,12 @@ public:
// reinterpret_cast is needed here :'(
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
throwErrorIfNeeded(rc, "Bind string (as bytes)");
} else if constexpr (std::is_same_v<DecayedType, UintTupleType>) {
} else if constexpr (std::is_same_v<DecayedType, UintTupleType> || std::is_same_v<DecayedType, UintByteTupleType>) {
auto const rc = cass_statement_bind_tuple(*this, idx, Tuple{std::forward<Type>(value)});
throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32>");
throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32> or <uint32_t, ripple::uint256>");
} else if constexpr (std::is_same_v<DecayedType, ByteVectorType>) {
auto const rc = cass_statement_bind_collection(*this, idx, Collection{std::forward<Type>(value)});
throwErrorIfNeeded(rc, "Bind collection");
} else if constexpr (std::is_same_v<DecayedType, bool>) {
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);
throwErrorIfNeeded(rc, "Bind bool");

View File

@@ -21,6 +21,7 @@
#include <data/cassandra/impl/ManagedObject.h>
#include <ripple/basics/base_uint.h>
#include <cassandra.h>
#include <functional>
@@ -76,6 +77,14 @@ public:
else if constexpr (std::is_convertible_v<DecayedType, int64_t>) {
auto const rc = cass_tuple_set_int64(*this, idx, value);
throwErrorIfNeeded(rc, "Bind int64");
} else if constexpr (std::is_same_v<DecayedType, ripple::uint256>) {
auto const rc = cass_tuple_set_bytes(
*this,
idx,
static_cast<cass_byte_t const*>(static_cast<unsigned char const*>(value.data())),
value.size()
);
throwErrorIfNeeded(rc, "Bind ripple::uint256");
} else {
// type not supported for binding
static_assert(unsupported_v<DecayedType>);

View File

@@ -43,6 +43,7 @@
#include <rpc/handlers/NFTHistory.h>
#include <rpc/handlers/NFTInfo.h>
#include <rpc/handlers/NFTSellOffers.h>
#include <rpc/handlers/NFTsByIssuer.h>
#include <rpc/handlers/NoRippleCheck.h>
#include <rpc/handlers/Ping.h>
#include <rpc/handlers/Random.h>
@@ -80,7 +81,8 @@ ProductionHandlerProvider::ProductionHandlerProvider(
{"ledger_data", {LedgerDataHandler{backend}}},
{"ledger_entry", {LedgerEntryHandler{backend}}},
{"ledger_range", {LedgerRangeHandler{backend}}},
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}},
{"nft_info", {NFTInfoHandler{backend}, true}}, // clio only
{"nft_sell_offers", {NFTSellOffersHandler{backend}}},

View File

@@ -46,6 +46,11 @@ NFTInfoHandler::process(NFTInfoHandler::Input input, Context const& ctx) const
if (not maybeNft.has_value())
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "NFT not found"}};
// TODO - this formatting is exactly the same and SHOULD REMAIN THE SAME
// for each element of the `nfts_by_issuer` API. We should factor this out
// so that the formats don't diverge. In the mean time, do not make any
// changes to this formatting without making the same changes to that
// formatting.
auto const& nft = *maybeNft;
auto output = NFTInfoHandler::Output{};

View File

@@ -0,0 +1,135 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/protocol/nft.h>
#include <rpc/RPCHelpers.h>
#include <rpc/handlers/NFTsByIssuer.h>
using namespace ripple;
namespace rpc {
NFTsByIssuerHandler::Result
NFTsByIssuerHandler::process(NFTsByIssuerHandler::Input input, Context const& ctx) const
{
auto const range = sharedPtrBackend_->fetchLedgerRange();
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq(
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
);
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
return Error{*status};
auto const lgrInfo = std::get<LedgerInfo>(lgrInfoOrStatus);
auto const limit = input.limit.value_or(NFTsByIssuerHandler::LIMIT_DEFAULT);
auto const issuer = accountFromStringStrict(input.issuer);
auto const accountLedgerObject =
sharedPtrBackend_->fetchLedgerObject(ripple::keylet::account(*issuer).key, lgrInfo.seq, ctx.yield);
if (!accountLedgerObject)
return Error{Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"}};
std::optional<uint256> cursor;
if (input.marker)
cursor = uint256{input.marker->c_str()};
auto const dbResponse =
sharedPtrBackend_->fetchNFTsByIssuer(*issuer, input.nftTaxon, lgrInfo.seq, limit, cursor, ctx.yield);
auto output = NFTsByIssuerHandler::Output{};
output.issuer = toBase58(*issuer);
output.limit = limit;
output.ledgerIndex = lgrInfo.seq;
output.nftTaxon = input.nftTaxon;
for (auto const& nft : dbResponse.nfts) {
boost::json::object nftJson;
nftJson[JS(nft_id)] = strHex(nft.tokenID);
nftJson[JS(ledger_index)] = nft.ledgerSequence;
nftJson[JS(owner)] = toBase58(nft.owner);
nftJson["is_burned"] = nft.isBurned;
nftJson[JS(uri)] = strHex(nft.uri);
nftJson[JS(flags)] = nft::getFlags(nft.tokenID);
nftJson["transfer_fee"] = nft::getTransferFee(nft.tokenID);
nftJson[JS(issuer)] = toBase58(nft::getIssuer(nft.tokenID));
nftJson["nft_taxon"] = nft::toUInt32(nft::getTaxon(nft.tokenID));
nftJson[JS(nft_serial)] = nft::getSerial(nft.tokenID);
output.nfts.push_back(nftJson);
}
if (dbResponse.cursor.has_value())
output.marker = strHex(*dbResponse.cursor);
return output;
}
void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, NFTsByIssuerHandler::Output const& output)
{
jv = {
{JS(issuer), output.issuer},
{JS(limit), output.limit},
{JS(ledger_index), output.ledgerIndex},
{"nfts", output.nfts},
{JS(validated), output.validated},
};
if (output.marker.has_value())
jv.as_object()[JS(marker)] = *(output.marker);
if (output.nftTaxon.has_value())
jv.as_object()["nft_taxon"] = *(output.nftTaxon);
}
NFTsByIssuerHandler::Input
tag_invoke(boost::json::value_to_tag<NFTsByIssuerHandler::Input>, boost::json::value const& jv)
{
auto const& jsonObject = jv.as_object();
NFTsByIssuerHandler::Input input;
input.issuer = jsonObject.at(JS(issuer)).as_string().c_str();
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str();
if (jsonObject.contains(JS(ledger_index))) {
if (!jsonObject.at(JS(ledger_index)).is_string()) {
input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64();
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str());
}
}
if (jsonObject.contains(JS(limit)))
input.limit = jsonObject.at(JS(limit)).as_int64();
if (jsonObject.contains("nft_taxon"))
input.nftTaxon = jsonObject.at("nft_taxon").as_int64();
if (jsonObject.contains(JS(marker)))
input.marker = jsonObject.at(JS(marker)).as_string().c_str();
return input;
}
} // namespace rpc

View File

@@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/BackendInterface.h>
#include <rpc/RPCHelpers.h>
#include <rpc/common/Modifiers.h>
#include <rpc/common/Types.h>
#include <rpc/common/Validators.h>
namespace rpc {
class NFTsByIssuerHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
public:
static auto constexpr LIMIT_MIN = 1;
static auto constexpr LIMIT_MAX = 100;
static auto constexpr LIMIT_DEFAULT = 50;
struct Output {
boost::json::array nfts;
uint32_t ledgerIndex;
std::string issuer;
bool validated = true;
std::optional<uint32_t> nftTaxon;
uint32_t limit;
std::optional<std::string> marker;
};
struct Input {
std::string issuer;
std::optional<uint32_t> nftTaxon;
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
std::optional<std::string> marker;
std::optional<uint32_t> limit;
};
using Result = HandlerReturnType<Output>;
NFTsByIssuerHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend)
{
}
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const rpcSpec = RpcSpec{
{JS(issuer), validation::Required{}, validation::AccountValidator},
{"nft_taxon", validation::Type<uint32_t>{}},
{JS(ledger_hash), validation::Uint256HexStringValidator},
{JS(ledger_index), validation::LedgerIndexValidator},
{JS(limit),
validation::Type<uint32_t>{},
validation::Min(1u),
modifiers::Clamp<int32_t>{LIMIT_MIN, LIMIT_MAX}},
{JS(marker), validation::Uint256HexStringValidator},
};
return rpcSpec;
}
Result
process(Input input, Context const& ctx) const;
private:
friend void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
friend Input
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
};
} // namespace rpc

View File

@@ -385,7 +385,7 @@ TEST_F(RPCNFTInfoHandlerTest, BurnedNFT)
});
}
// nft is not burned and uri is not available -> should specify null
// uri is not available -> should specify an empty string
TEST_F(RPCNFTInfoHandlerTest, NotBurnedNFTWithoutURI)
{
constexpr static auto currentOutput = R"({

View File

@@ -0,0 +1,670 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <rpc/common/AnyHandler.h>
#include <rpc/handlers/NFTsByIssuer.h>
#include <util/Fixtures.h>
#include <util/TestObject.h>
#include <fmt/core.h>
using namespace rpc;
namespace json = boost::json;
using namespace testing;
constexpr static auto ACCOUNT = "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67";
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto NFTID1 = "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F0000099B00000000"; // taxon 0
constexpr static auto NFTID2 = "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F16E5DA9C00000001"; // taxon 0
constexpr static auto NFTID3 = "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F5B974D9E00000004"; // taxon 1
static std::string NFT1OUT =
R"({
"nft_id": "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F0000099B00000000",
"ledger_index": 29,
"owner": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"is_burned": false,
"uri": "757269",
"flags": 8,
"transfer_fee": 0,
"issuer": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"nft_taxon": 0,
"nft_serial": 0
})";
static std::string NFT2OUT =
R"({
"nft_id": "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F16E5DA9C00000001",
"ledger_index": 29,
"owner": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"is_burned": false,
"uri": "757269",
"flags": 8,
"transfer_fee": 0,
"issuer": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"nft_taxon": 0,
"nft_serial": 1
})";
static std::string NFT3OUT =
R"({
"nft_id": "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F5B974D9E00000004",
"ledger_index": 29,
"owner": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"is_burned": false,
"uri": "757269",
"flags": 8,
"transfer_fee": 0,
"issuer": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"nft_taxon": 1,
"nft_serial": 4
})";
class RPCNFTsByIssuerHandlerTest : public HandlerBaseTest {};
TEST_F(RPCNFTsByIssuerHandlerTest, NonHexLedgerHash)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_hash": "xxx"
}})",
ACCOUNT
));
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashMalformed");
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, NonStringLedgerHash)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_hash": 123
}})",
ACCOUNT
));
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashNotString");
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, InvalidLedgerIndexString)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_index": "notvalidated"
}})",
ACCOUNT
));
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed");
});
}
// error case: issuer invalid format, length is incorrect
TEST_F(RPCNFTsByIssuerHandlerTest, NFTIssuerInvalidFormat)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(R"({
"issuer": "xxx"
})");
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "actMalformed");
EXPECT_EQ(err.at("error_message").as_string(), "issuerMalformed");
});
}
// error case: issuer missing
TEST_F(RPCNFTsByIssuerHandlerTest, NFTIssuerMissing)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(R"({})");
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Required field 'issuer' missing");
});
}
// error case: issuer invalid format
TEST_F(RPCNFTsByIssuerHandlerTest, NFTIssuerNotString)
{
runSpawn([this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const input = json::parse(R"({
"issuer": 12
})");
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "issuerNotString");
});
}
// error case ledger non exist via hash
TEST_F(RPCNFTsByIssuerHandlerTest, NonExistLedgerViaLedgerHash)
{
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
// mock fetchLedgerByHash return empty
EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1);
ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _))
.WillByDefault(Return(std::optional<ripple::LedgerInfo>{}));
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
LEDGERHASH
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
// error case ledger non exist via index
TEST_F(RPCNFTsByIssuerHandlerTest, NonExistLedgerViaLedgerStringIndex)
{
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
// mock fetchLedgerBySequence return empty
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerInfo>{}));
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_index": "4"
}})",
ACCOUNT
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, NonExistLedgerViaLedgerIntIndex)
{
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
// mock fetchLedgerBySequence return empty
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerInfo>{}));
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_index": 4
}})",
ACCOUNT
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
// error case ledger > max seq via hash
// idk why this case will happen in reality
TEST_F(RPCNFTsByIssuerHandlerTest, NonExistLedgerViaLedgerHash2)
{
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
// mock fetchLedgerByHash return ledger but seq is 31 > 30
auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 31);
ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerinfo));
EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
LEDGERHASH
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
// error case ledger > max seq via index
TEST_F(RPCNFTsByIssuerHandlerTest, NonExistLedgerViaLedgerIndex2)
{
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
// no need to check from db,call fetchLedgerBySequence 0 time
// differ from previous logic
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(0);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_index": "31"
}})",
ACCOUNT
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto const handler = AnyHandler{NFTsByIssuerHandler{mockBackendPtr}};
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
// normal case when issuer does not exist or has no NFTs
TEST_F(RPCNFTsByIssuerHandlerTest, AccountNotFound)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": 30,
"nfts": [],
"validated": true
}})",
ACCOUNT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30);
ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerinfo));
EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1);
ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(std::optional<Blob>{}));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
LEDGERHASH
));
runSpawn([&, this](boost::asio::yield_context yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "actNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound");
});
}
// normal case when issuer has a single nft
TEST_F(RPCNFTsByIssuerHandlerTest, DefaultParameters)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": 30,
"nfts": [{}],
"validated": true
}})",
ACCOUNT,
NFT1OUT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {CreateNFT(NFTID1, ACCOUNT, 29)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, {}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(
account, testing::Eq(std::nullopt), Const(30), testing::_, testing::Eq(std::nullopt), testing::_
)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}"
}})",
ACCOUNT
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, SpecificLedgerIndex)
{
auto const specificLedger = 20;
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": {},
"nfts": [{{
"nft_id": "00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F0000099B00000000",
"ledger_index": 20,
"owner": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"is_burned": false,
"uri": "757269",
"flags": 8,
"transfer_fee": 0,
"issuer": "r4X6JLsBfhNK4UnquNkCxhVHKPkvbQff67",
"nft_taxon": 0,
"nft_serial": 0
}}],
"validated": true
}})",
ACCOUNT,
specificLedger
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, specificLedger);
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(specificLedger, _)).WillByDefault(Return(ledgerInfo));
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, specificLedger, _))
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {CreateNFT(NFTID1, ACCOUNT, specificLedger)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, {}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(
account, testing::Eq(std::nullopt), Const(specificLedger), testing::_, testing::Eq(std::nullopt), testing::_
)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"ledger_index": {}
}})",
ACCOUNT,
specificLedger
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, TaxonParameter)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": 30,
"nfts": [{}],
"validated": true,
"nft_taxon": 0
}})",
ACCOUNT,
NFT1OUT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {CreateNFT(NFTID1, ACCOUNT, 29)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, {}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(account, testing::Optional(0), Const(30), testing::_, testing::Eq(std::nullopt), testing::_)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"nft_taxon": 0
}})",
ACCOUNT
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, MarkerParameter)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": 30,
"nfts": [{}],
"validated": true,
"marker":"00080000EC28C2910FD1C454A51598AAB91C8876286B2E7F5B974D9E00000004"
}})",
ACCOUNT,
NFT3OUT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {CreateNFT(NFTID3, ACCOUNT, 29)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, ripple::uint256{NFTID3}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(account, testing::_, Const(30), testing::_, testing::Eq(ripple::uint256{NFTID1}), testing::_)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"marker": "{}"
}})",
ACCOUNT,
NFTID1
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, MultipleNFTs)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":50,
"ledger_index": 30,
"nfts": [{}, {}, {}],
"validated": true
}})",
ACCOUNT,
NFT1OUT,
NFT2OUT,
NFT3OUT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {
CreateNFT(NFTID1, ACCOUNT, 29), CreateNFT(NFTID2, ACCOUNT, 29), CreateNFT(NFTID3, ACCOUNT, 29)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, {}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(
account, testing::Eq(std::nullopt), Const(30), testing::_, testing::Eq(std::nullopt), testing::_
)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}"
}})",
ACCOUNT
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}
TEST_F(RPCNFTsByIssuerHandlerTest, LimitMoreThanMAx)
{
auto const currentOutput = fmt::format(
R"({{
"issuer": "{}",
"limit":100,
"ledger_index": 30,
"nfts": [{}],
"validated": true
}})",
ACCOUNT,
NFT1OUT
);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(30); // max
auto ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<NFT> const nfts = {CreateNFT(NFTID1, ACCOUNT, 29)};
auto const account = GetAccountIDWithString(ACCOUNT);
ON_CALL(*rawBackendPtr, fetchNFTsByIssuer).WillByDefault(Return(NFTsAndCursor{nfts, {}}));
EXPECT_CALL(
*rawBackendPtr,
fetchNFTsByIssuer(
account,
testing::Eq(std::nullopt),
Const(30),
Const(NFTsByIssuerHandler::LIMIT_MAX),
testing::Eq(std::nullopt),
testing::_
)
)
.Times(1);
auto const input = json::parse(fmt::format(
R"({{
"issuer": "{}",
"limit": {}
}})",
ACCOUNT,
NFTsByIssuerHandler::LIMIT_MAX + 1
));
runSpawn([&, this](auto& yield) {
auto handler = AnyHandler{NFTsByIssuerHandler{this->mockBackendPtr}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(currentOutput), *output);
});
}

View File

@@ -107,6 +107,18 @@ struct MockBackend : public BackendInterface {
(const, override)
);
MOCK_METHOD(
NFTsAndCursor,
fetchNFTsByIssuer,
(ripple::AccountID const& issuer,
std::optional<std::uint32_t> const& taxon,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursorIn,
boost::asio::yield_context yield),
(const, override)
);
MOCK_METHOD(
std::vector<Blob>,
doFetchLedgerObjects,