mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 20:05:51 +00:00
@@ -126,6 +126,7 @@ target_sources (clio PRIVATE
|
|||||||
src/rpc/handlers/LedgerData.cpp
|
src/rpc/handlers/LedgerData.cpp
|
||||||
src/rpc/handlers/LedgerEntry.cpp
|
src/rpc/handlers/LedgerEntry.cpp
|
||||||
src/rpc/handlers/LedgerRange.cpp
|
src/rpc/handlers/LedgerRange.cpp
|
||||||
|
src/rpc/handlers/NFTsByIssuer.cpp
|
||||||
src/rpc/handlers/NFTBuyOffers.cpp
|
src/rpc/handlers/NFTBuyOffers.cpp
|
||||||
src/rpc/handlers/NFTHistory.cpp
|
src/rpc/handlers/NFTHistory.cpp
|
||||||
src/rpc/handlers/NFTInfo.cpp
|
src/rpc/handlers/NFTInfo.cpp
|
||||||
@@ -205,6 +206,7 @@ if (tests)
|
|||||||
unittests/rpc/handlers/RandomTests.cpp
|
unittests/rpc/handlers/RandomTests.cpp
|
||||||
unittests/rpc/handlers/NFTInfoTests.cpp
|
unittests/rpc/handlers/NFTInfoTests.cpp
|
||||||
unittests/rpc/handlers/NFTBuyOffersTests.cpp
|
unittests/rpc/handlers/NFTBuyOffersTests.cpp
|
||||||
|
unittests/rpc/handlers/NFTsByIssuerTest.cpp
|
||||||
unittests/rpc/handlers/NFTSellOffersTests.cpp
|
unittests/rpc/handlers/NFTSellOffersTests.cpp
|
||||||
unittests/rpc/handlers/NFTHistoryTests.cpp
|
unittests/rpc/handlers/NFTHistoryTests.cpp
|
||||||
unittests/rpc/handlers/SubscribeTests.cpp
|
unittests/rpc/handlers/SubscribeTests.cpp
|
||||||
|
|||||||
@@ -290,6 +290,28 @@ public:
|
|||||||
boost::asio::yield_context yield
|
boost::asio::yield_context yield
|
||||||
) const = 0;
|
) 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.
|
* @brief Fetches a specific ledger object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -441,6 +441,96 @@ public:
|
|||||||
return {txns, {}};
|
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>
|
std::optional<Blob>
|
||||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
||||||
const override
|
const override
|
||||||
|
|||||||
@@ -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.
|
* @brief Stores a range of sequences as a min and max pair.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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]() {
|
PreparedStatement selectNFTURI = [this]() {
|
||||||
return handle_.get().prepare(fmt::format(
|
return handle_.get().prepare(fmt::format(
|
||||||
R"(
|
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]() {
|
PreparedStatement selectNFTTx = [this]() {
|
||||||
return handle_.get().prepare(fmt::format(
|
return handle_.get().prepare(fmt::format(
|
||||||
R"(
|
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]() {
|
PreparedStatement selectLedgerByHash = [this]() {
|
||||||
return handle_.get().prepare(fmt::format(
|
return handle_.get().prepare(fmt::format(
|
||||||
R"(
|
R"(
|
||||||
|
|||||||
87
src/data/cassandra/impl/Collection.h
Normal file
87
src/data/cassandra/impl/Collection.h
Normal 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
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <data/cassandra/Types.h>
|
#include <data/cassandra/Types.h>
|
||||||
|
#include <data/cassandra/impl/Collection.h>
|
||||||
#include <data/cassandra/impl/ManagedObject.h>
|
#include <data/cassandra/impl/ManagedObject.h>
|
||||||
#include <data/cassandra/impl/Tuple.h>
|
#include <data/cassandra/impl/Tuple.h>
|
||||||
#include <util/Expected.h>
|
#include <util/Expected.h>
|
||||||
@@ -99,6 +100,8 @@ public:
|
|||||||
using DecayedType = std::decay_t<Type>;
|
using DecayedType = std::decay_t<Type>;
|
||||||
using UCharVectorType = std::vector<unsigned char>;
|
using UCharVectorType = std::vector<unsigned char>;
|
||||||
using UintTupleType = std::tuple<uint32_t, uint32_t>;
|
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>) {
|
if constexpr (std::is_same_v<DecayedType, ripple::uint256>) {
|
||||||
auto const rc = bindBytes(value.data(), value.size());
|
auto const rc = bindBytes(value.data(), value.size());
|
||||||
@@ -113,9 +116,12 @@ public:
|
|||||||
// reinterpret_cast is needed here :'(
|
// reinterpret_cast is needed here :'(
|
||||||
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
|
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
|
||||||
throwErrorIfNeeded(rc, "Bind string (as bytes)");
|
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)});
|
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>) {
|
} else if constexpr (std::is_same_v<DecayedType, bool>) {
|
||||||
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);
|
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);
|
||||||
throwErrorIfNeeded(rc, "Bind bool");
|
throwErrorIfNeeded(rc, "Bind bool");
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <data/cassandra/impl/ManagedObject.h>
|
#include <data/cassandra/impl/ManagedObject.h>
|
||||||
|
|
||||||
|
#include <ripple/basics/base_uint.h>
|
||||||
#include <cassandra.h>
|
#include <cassandra.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -76,6 +77,14 @@ public:
|
|||||||
else if constexpr (std::is_convertible_v<DecayedType, int64_t>) {
|
else if constexpr (std::is_convertible_v<DecayedType, int64_t>) {
|
||||||
auto const rc = cass_tuple_set_int64(*this, idx, value);
|
auto const rc = cass_tuple_set_int64(*this, idx, value);
|
||||||
throwErrorIfNeeded(rc, "Bind int64");
|
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 {
|
} else {
|
||||||
// type not supported for binding
|
// type not supported for binding
|
||||||
static_assert(unsupported_v<DecayedType>);
|
static_assert(unsupported_v<DecayedType>);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#include <rpc/handlers/NFTHistory.h>
|
#include <rpc/handlers/NFTHistory.h>
|
||||||
#include <rpc/handlers/NFTInfo.h>
|
#include <rpc/handlers/NFTInfo.h>
|
||||||
#include <rpc/handlers/NFTSellOffers.h>
|
#include <rpc/handlers/NFTSellOffers.h>
|
||||||
|
#include <rpc/handlers/NFTsByIssuer.h>
|
||||||
#include <rpc/handlers/NoRippleCheck.h>
|
#include <rpc/handlers/NoRippleCheck.h>
|
||||||
#include <rpc/handlers/Ping.h>
|
#include <rpc/handlers/Ping.h>
|
||||||
#include <rpc/handlers/Random.h>
|
#include <rpc/handlers/Random.h>
|
||||||
@@ -80,7 +81,8 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
|||||||
{"ledger_data", {LedgerDataHandler{backend}}},
|
{"ledger_data", {LedgerDataHandler{backend}}},
|
||||||
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
||||||
{"ledger_range", {LedgerRangeHandler{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_buy_offers", {NFTBuyOffersHandler{backend}}},
|
||||||
{"nft_info", {NFTInfoHandler{backend}, true}}, // clio only
|
{"nft_info", {NFTInfoHandler{backend}, true}}, // clio only
|
||||||
{"nft_sell_offers", {NFTSellOffersHandler{backend}}},
|
{"nft_sell_offers", {NFTSellOffersHandler{backend}}},
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ NFTInfoHandler::process(NFTInfoHandler::Input input, Context const& ctx) const
|
|||||||
if (not maybeNft.has_value())
|
if (not maybeNft.has_value())
|
||||||
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "NFT not found"}};
|
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 const& nft = *maybeNft;
|
||||||
auto output = NFTInfoHandler::Output{};
|
auto output = NFTInfoHandler::Output{};
|
||||||
|
|
||||||
|
|||||||
135
src/rpc/handlers/NFTsByIssuer.cpp
Normal file
135
src/rpc/handlers/NFTsByIssuer.cpp
Normal 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
|
||||||
90
src/rpc/handlers/NFTsByIssuer.h
Normal file
90
src/rpc/handlers/NFTsByIssuer.h
Normal 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
|
||||||
@@ -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)
|
TEST_F(RPCNFTInfoHandlerTest, NotBurnedNFTWithoutURI)
|
||||||
{
|
{
|
||||||
constexpr static auto currentOutput = R"({
|
constexpr static auto currentOutput = R"({
|
||||||
|
|||||||
670
unittests/rpc/handlers/NFTsByIssuerTest.cpp
Normal file
670
unittests/rpc/handlers/NFTsByIssuerTest.cpp
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -107,6 +107,18 @@ struct MockBackend : public BackendInterface {
|
|||||||
(const, override)
|
(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(
|
MOCK_METHOD(
|
||||||
std::vector<Blob>,
|
std::vector<Blob>,
|
||||||
doFetchLedgerObjects,
|
doFetchLedgerObjects,
|
||||||
|
|||||||
Reference in New Issue
Block a user