mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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"(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
#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");
 | 
			
		||||
 
 | 
			
		||||
@@ -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>);
 | 
			
		||||
 
 | 
			
		||||
@@ -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}}},
 | 
			
		||||
 
 | 
			
		||||
@@ -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{};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
{
 | 
			
		||||
    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)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user