From be32b860a2d32c665e0a16ef4eb8e572f428ea37 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Fri, 25 Aug 2023 15:55:32 -0400 Subject: [PATCH] initial commit rval param --- CMakeLists.txt | 2 + src/data/BackendInterface.h | 18 ++++ src/data/CassandraBackend.h | 112 ++++++++++++++++++++ src/data/DBHelpers.h | 37 +++++++ src/data/Types.h | 6 ++ src/data/cassandra/Schema.h | 36 +++++++ src/etl/CFTHelpers.cpp | 81 +++++++++++++++ src/etl/CFTHelpers.h | 73 +++++++++++++ src/etl/impl/AsyncData.h | 7 ++ src/etl/impl/LedgerLoader.h | 7 ++ src/etl/impl/Transformer.h | 1 + src/rpc/common/impl/HandlerProvider.cpp | 2 + src/rpc/handlers/CFTsByIssuer.cpp | 132 ++++++++++++++++++++++++ src/rpc/handlers/CFTsByIssuer.h | 109 +++++++++++++++++++ unittests/util/MockBackend.h | 10 ++ 15 files changed, 633 insertions(+) create mode 100644 src/etl/CFTHelpers.cpp create mode 100644 src/etl/CFTHelpers.h create mode 100644 src/rpc/handlers/CFTsByIssuer.cpp create mode 100644 src/rpc/handlers/CFTsByIssuer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 953702896..fd30fd5bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ target_sources (clio PRIVATE src/etl/ETLService.cpp src/etl/LoadBalancer.cpp src/etl/impl/ForwardCache.cpp + src/etl/CFTHelpers.cpp ## Feed src/feed/SubscriptionManager.cpp ## Web @@ -113,6 +114,7 @@ target_sources (clio PRIVATE src/rpc/handlers/AccountTx.cpp src/rpc/handlers/BookChanges.cpp src/rpc/handlers/BookOffers.cpp + src/rpc/handlers/CFTsByIssuer.cpp src/rpc/handlers/DepositAuthorized.cpp src/rpc/handlers/GatewayBalances.cpp src/rpc/handlers/Ledger.cpp diff --git a/src/data/BackendInterface.h b/src/data/BackendInterface.h index 1a25f6d57..211095393 100644 --- a/src/data/BackendInterface.h +++ b/src/data/BackendInterface.h @@ -295,6 +295,15 @@ class BackendInterface std::optional const& cursorIn, boost::asio::yield_context yield) const = 0; + virtual + CFTIssuancesAndCursor + fetchIssuerCFTs( + ripple::AccountID const& issuer, + std::uint32_t const limit, + std::optional const& cursorIn, + std::uint32_t const ledgerSequence, + boost::asio::yield_context yield) const = 0; + /** * @brief Fetches a specific ledger object. * @@ -352,6 +361,12 @@ class BackendInterface std::uint32_t const sequence, boost::asio::yield_context yield) const = 0; + virtual std::vector>> + doFetchLedgerObjectsPair( + std::vector const& keys, + std::uint32_t const sequence, + boost::asio::yield_context yield) const = 0; + /** * @brief Returns the difference between ledgers. * @@ -520,6 +535,9 @@ class BackendInterface virtual void writeNFTTransactions(std::vector&& data) = 0; + virtual void + writeCFTIssuancePairs(std::vector>&& data) = 0; + /** * @brief Write a new successor. * diff --git a/src/data/CassandraBackend.h b/src/data/CassandraBackend.h index c9ab52722..0c48bf5fc 100644 --- a/src/data/CassandraBackend.h +++ b/src/data/CassandraBackend.h @@ -482,6 +482,102 @@ class BasicCassandraBackend : public BackendInterface return {txns, {}}; } + CFTIssuancesAndCursor + fetchIssuerCFTs(ripple::AccountID const& issuer, + std::uint32_t const limit, + std::optional const& cursorIn, + std::uint32_t const ledgerSequence, + boost::asio::yield_context yield)const override + { + // Statement statement = schema_->selectIssuerCFTs.bind(issuer); + // statement.bindAt(1, cursorIn.value_or(ripple::uint256(0))); + // statement.bindAt(2, Limit{limit}); + // auto const issuerRes = executor_.read(yield, statement); + + auto const issuerRes = executor_.read(yield, schema_->selectIssuerCFTs, issuer, cursorIn.value_or(ripple::uint256(0)), Limit{limit}); + + auto const& issuerResults = issuerRes.value(); + if (not issuerResults.hasRows()) + { + LOG(log_.debug()) << "No rows returned"; + return {}; + } + + std::vector cftIssuances; + std::optional cursor; + for (auto [cftIssuanceID] : extract(issuerResults)) + { + cftIssuances.push_back(cftIssuanceID); + cursor = cftIssuanceID; + } + + auto const maybeCFTIssuanceObjects = doFetchLedgerObjectsPair(cftIssuances, ledgerSequence, yield); + + std::vector> cftIssuanceObjects; + + //need to filter out the objs that don't exist at the ledger seq because these CFTs are in no particular time order + for(size_t i = 0; i < cftIssuances.size(); i++){ + if(!maybeCFTIssuanceObjects[i].has_value()) + continue; + + auto const objSeqPair = maybeCFTIssuanceObjects[i].value(); + cftIssuanceObjects.push_back(std::make_tuple(cftIssuances[i], objSeqPair.first, objSeqPair.second)); + + } + + if(cftIssuances.size() == limit) + return {cftIssuanceObjects, cursor}; + + return {cftIssuanceObjects, {}}; + } + + std::vector>> + doFetchLedgerObjectsPair( + std::vector const& keys, + std::uint32_t const sequence, + boost::asio::yield_context yield) const override + { + if (keys.size() == 0) + return {}; + + auto const numKeys = keys.size(); + LOG(log_.trace()) << "Fetching " << numKeys << " objects"; + + std::vector>> results; + results.reserve(numKeys); + + std::vector statements; + statements.reserve(numKeys); + + // TODO: seems like a job for "key IN (list of keys)" instead? + std::transform( + std::cbegin(keys), std::cend(keys), std::back_inserter(statements), [this, &sequence](auto const& key) { + return schema_->selectObject.bind(key, sequence); + }); + + auto const entries = executor_.readEach(yield, statements); + if(entries.size() != statements.size()) + { + LOG(log_.debug()) << "Input and output vector sizes are different"; + return {}; + } + + + std::transform( + std::cbegin(entries), std::cend(entries), std::back_inserter(results), [](auto const& res) -> std::optional> { + if (auto const maybeValue = res.template get(); maybeValue){ + auto [object, seq] = *maybeValue; + return std::make_pair(object, seq); + } + else + return {}; + }); + + assert(numKeys == results.size()); + LOG(log_.trace()) << "Fetched " << numKeys << " objects"; + return results; + } + std::optional doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield) const override @@ -783,6 +879,22 @@ class BasicCassandraBackend : public BackendInterface executor_.write(std::move(statements)); } + void + writeCFTIssuancePairs(std::vector>&& data) override + { + std::vector statements; + for (auto const& record : data) + { + auto const token_id = record.first; + auto const issuer = record.second; + + statements.push_back( + schema_->insertIssuerCFT.bind(issuer, token_id)); + } + + executor_.write(std::move(statements)); + } + void startWrites() const override { diff --git a/src/data/DBHelpers.h b/src/data/DBHelpers.h index 85139b240..68967bc74 100644 --- a/src/data/DBHelpers.h +++ b/src/data/DBHelpers.h @@ -251,3 +251,40 @@ uint256ToString(ripple::uint256 const& input) /** @brief The ripple epoch start timestamp. Midnight on 1st January 2000. */ static constexpr std::uint32_t rippleEpochStart = 946684800; + + +/** + * @brief Represents an CFT state at a particular ledger. + * + */ +struct CFTsData +{ + ripple::uint256 cftIssuanceID; + ripple::AccountID holder; + std::uint32_t ledgerSequence; + std::uint32_t flags; + std::uint64_t amount; + std::uint64_t lockedAmount; + bool isUntrusted = false; + + + CFTsData( + ripple::uint256 const& cftIssuanceID, + ripple::AccountID const& holder, + std::uint32_t ledgerSequence, + std::uint32_t flags, + std::uint64_t amount, + std::uint64_t lockedAmount, + bool isUntrusted) + : cftIssuanceID(cftIssuanceID), + holder(holder), + ledgerSequence(ledgerSequence), + flags(flags), + amount(amount), + lockedAmount(lockedAmount), + isUntrusted(isUntrusted) + { + } + + +}; diff --git a/src/data/Types.h b/src/data/Types.h index 5d973dc61..7745c78b2 100644 --- a/src/data/Types.h +++ b/src/data/Types.h @@ -175,6 +175,12 @@ struct NFT } }; +struct CFTIssuancesAndCursor +{ + std::vector> cftIssuances; + std::optional cursor; +}; + /** * @brief Stores a range of sequences as a min and max pair. */ diff --git a/src/data/cassandra/Schema.h b/src/data/cassandra/Schema.h index 7cd8ff95c..ea57ae5e3 100644 --- a/src/data/cassandra/Schema.h +++ b/src/data/cassandra/Schema.h @@ -252,6 +252,19 @@ class Schema qualifiedTableName(settingsProvider_.get(), "nf_token_transactions"), settingsProvider_.get().getTtl())); + statements.emplace_back(fmt::format( + R"( + CREATE TABLE IF NOT EXISTS {} + ( + issuer blob, + token_id blob, + PRIMARY KEY (issuer, token_id) + ) + WITH CLUSTERING ORDER BY (token_id ASC) + AND default_time_to_live = {} + )", + qualifiedTableName(settingsProvider_.get(), "issuer_cf_tokens"), + settingsProvider_.get().getTtl())); return statements; }(); @@ -393,6 +406,15 @@ class Schema qualifiedTableName(settingsProvider_.get(), "ledger_hashes"))); }(); + PreparedStatement insertIssuerCFT = [this]() { + return handle_.get().prepare(fmt::format( + R"( + INSERT INTO {} + (issuer, token_id) + VALUES (?, ?) + )", + qualifiedTableName(settingsProvider_.get(), "issuer_cf_tokens"))); + }(); // // Update (and "delete") queries // @@ -633,6 +655,20 @@ class Schema )", qualifiedTableName(settingsProvider_.get(), "ledger_range"))); }(); + + PreparedStatement selectIssuerCFTs = [this]() { + return handle_.get().prepare(fmt::format( + R"( + SELECT token_id + FROM {} + WHERE issuer = ? + AND token_id > ? + ORDER BY token_id ASC + LIMIT ? + )", + qualifiedTableName(settingsProvider_.get(), "issuer_cf_tokens"))); + }(); + }; /** diff --git a/src/etl/CFTHelpers.cpp b/src/etl/CFTHelpers.cpp new file mode 100644 index 000000000..523bf56f9 --- /dev/null +++ b/src/etl/CFTHelpers.cpp @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +#include +#include +#include +#include + +namespace etl { + +std::optional> +getCFTokenIssuanceCreate(ripple::TxMeta const& txMeta, ripple::STTx const& sttx){ + + ripple::AccountID issuer = sttx.getAccountID(ripple::sfAccount); + + for (ripple::STObject const& node : txMeta.getNodes()) + { + if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltCFTOKEN_ISSUANCE) + continue; + + if (node.getFName() == ripple::sfCreatedNode) + return std::make_pair(node.getFieldH256(ripple::sfLedgerIndex), issuer); + } + return {}; +} + +std::optional> +getCFTIssuancePairFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx){ + if (txMeta.getResultTER() != ripple::tesSUCCESS + || sttx.getTxnType()!= ripple::TxType::ttCFTOKEN_ISSUANCE_CREATE) + return {}; + + return getCFTokenIssuanceCreate(txMeta, sttx); +} + +std::optional> +getCFTIssuancePairFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob){ + ripple::STLedgerEntry const sle = + ripple::STLedgerEntry(ripple::SerialIter{blob.data(), blob.size()}, ripple::uint256::fromVoid(key.data())); + + if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltCFTOKEN_ISSUANCE) + return {}; + + + auto const cftIssuanceID = ripple::uint256::fromVoid(key.data()); + auto const issuer = sle.getAccountID(ripple::sfIssuer); + + return std::make_pair(cftIssuanceID, issuer); +} + +// std::vector +// getCFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx){ + +// } + +// std::vector +// getCFTDataFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob){ + +// } +} // namespace etl \ No newline at end of file diff --git a/src/etl/CFTHelpers.h b/src/etl/CFTHelpers.h new file mode 100644 index 000000000..0a52d7f01 --- /dev/null +++ b/src/etl/CFTHelpers.h @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +/** @file */ +#pragma once + +#include + +#include +#include + +namespace etl { + +/** + * @brief Pull CFTIssuance data from TX via ETLService. + * + * @param txMeta Transaction metadata + * @param sttx The transaction + * @return The CFTIssuance and issuer pair as a optional + */ +std::optional> +getCFTIssuancePairFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx); + +/** + * @brief Pull CFTIssuance data from ledger object via loadInitialLedger. + * + * @param seq The ledger sequence to pull for + * @param key The owner key + * @param blob Object data as blob + * @return The CFTIssuance and issuer pair as a optional + */ +std::optional> +getCFTIssuancePairFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob); + + +/** + * @brief Pull CFT data from TX via ETLService. + * + * @param txMeta Transaction metadata + * @param sttx The transaction + * @return CFTsData vector + */ +std::vector +getCFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx); + +/** + * @brief Pull CFT data from ledger object via loadInitialLedger. + * + * @param seq The ledger sequence to pull for + * @param key The owner key + * @param blob Object data as blob + * @return The CFT data as a vector + */ +std::vector +getCFTDataFromObj(std::uint32_t const seq, std::string const& key, std::string const& blob); + +} // namespace etl \ No newline at end of file diff --git a/src/etl/impl/AsyncData.h b/src/etl/impl/AsyncData.h index 7d915d1f5..a56f164d4 100644 --- a/src/etl/impl/AsyncData.h +++ b/src/etl/impl/AsyncData.h @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -140,6 +141,12 @@ class AsyncCallData backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()}); lastKey_ = obj.key(); backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data())); + + // Need to convert result into a vector of size 1 + auto const maybeCFTIsssuancePair = getCFTIssuancePairFromObj(request_.ledger().sequence(), obj.key(), obj.data()); + if(maybeCFTIsssuancePair) + backend.writeCFTIssuancePairs({*maybeCFTIsssuancePair}); + backend.writeLedgerObject( std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())); } diff --git a/src/etl/impl/LedgerLoader.h b/src/etl/impl/LedgerLoader.h index 5c79e3614..362c379d0 100644 --- a/src/etl/impl/LedgerLoader.h +++ b/src/etl/impl/LedgerLoader.h @@ -39,6 +39,7 @@ struct FormattedTransactionsData std::vector accountTxData; std::vector nfTokenTxData; std::vector nfTokensData; + std::vector> cftIssuancePairData; }; namespace etl::detail { @@ -106,6 +107,11 @@ class LedgerLoader result.nfTokenTxData.insert(result.nfTokenTxData.end(), nftTxs.begin(), nftTxs.end()); if (maybeNFT) result.nfTokensData.push_back(*maybeNFT); + + // No need to unqiue this because CFTs cannot duplicate + auto const maybeCFTIssuancePair = getCFTIssuancePairFromTx(txMeta, sttx); + if(maybeCFTIssuancePair) + result.cftIssuancePairData.push_back(*maybeCFTIssuancePair); auto journal = ripple::debugLog(); result.accountTxData.emplace_back(txMeta, sttx.getTransactionID(), journal); @@ -246,6 +252,7 @@ class LedgerLoader backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData)); backend_->writeNFTs(std::move(insertTxResult.nfTokensData)); backend_->writeNFTTransactions(std::move(insertTxResult.nfTokenTxData)); + backend_->writeCFTIssuancePairs(std::move(insertTxResult.cftIssuancePairData)); } backend_->finishWrites(sequence); diff --git a/src/etl/impl/Transformer.h b/src/etl/impl/Transformer.h index 9b7f795e1..7517a4df1 100644 --- a/src/etl/impl/Transformer.h +++ b/src/etl/impl/Transformer.h @@ -199,6 +199,7 @@ class Transformer backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData)); backend_->writeNFTs(std::move(insertTxResultOp->nfTokensData)); backend_->writeNFTTransactions(std::move(insertTxResultOp->nfTokenTxData)); + backend_->writeCFTIssuancePairs(std::move(insertTxResultOp->cftIssuancePairData)); auto [success, duration] = ::util::timed>([&]() { return backend_->finishWrites(lgrInfo.seq); }); diff --git a/src/rpc/common/impl/HandlerProvider.cpp b/src/rpc/common/impl/HandlerProvider.cpp index 09178923d..132b691b8 100644 --- a/src/rpc/common/impl/HandlerProvider.cpp +++ b/src/rpc/common/impl/HandlerProvider.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,7 @@ ProductionHandlerProvider::ProductionHandlerProvider( {"account_tx", {AccountTxHandler{backend}}}, {"book_changes", {BookChangesHandler{backend}}}, {"book_offers", {BookOffersHandler{backend}}}, + {"cfts_by_issuer", {CFTsByIssuerHandler{backend}, true}}, {"deposit_authorized", {DepositAuthorizedHandler{backend}}}, {"gateway_balances", {GatewayBalancesHandler{backend}}}, {"ledger", {LedgerHandler{backend}}}, diff --git a/src/rpc/handlers/CFTsByIssuer.cpp b/src/rpc/handlers/CFTsByIssuer.cpp new file mode 100644 index 000000000..79df9efc6 --- /dev/null +++ b/src/rpc/handlers/CFTsByIssuer.cpp @@ -0,0 +1,132 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace rpc { + + +CFTsByIssuerHandler::Result +CFTsByIssuerHandler::process(CFTsByIssuerHandler::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(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + auto const accountID = accountFromStringStrict(input.issuer); + auto const limit = input.limit.value_or(LIMIT_DEFAULT); + bool const includeDeleted = input.includeDeleted.value_or(false); + std::optional const marker = + input.marker ? std::make_optional(ripple::uint256{input.marker->c_str()}) : std::nullopt; + + auto const cftIssuanceObjects = sharedPtrBackend_->fetchIssuerCFTs(*accountID, limit, marker, lgrInfo.seq, ctx.yield ); + + + auto output = CFTsByIssuerHandler::Output{}; + output.issuer = input.issuer; + output.ledgerIndex = lgrInfo.seq; + output.ledgerHash = ripple::strHex(lgrInfo.hash); + output.limit = input.limit; + if(cftIssuanceObjects.cursor.has_value()) + output.marker = ripple::strHex(cftIssuanceObjects.cursor.value()); + + boost::json::array cftIssuances; + for(auto const& [key, object, seq]: cftIssuanceObjects.cftIssuances){ + if(!object.size() && !includeDeleted) + continue; + + boost::json::object jsonObj; + + if(!object.size()){ + jsonObj["deleted_ledger_index"] = seq; + } + else{ + ripple::STLedgerEntry sle{ripple::SerialIter{object.data(), object.size()}, key}; + jsonObj = toBoostJson(sle.getJson(ripple::JsonOptions::none)).as_object(); + jsonObj.erase(JS(index)); + jsonObj[JS(ledger_index)] = seq; + // todo::may need to delete the ledger index that duplicates with the cftid + } + + jsonObj["CFTokenIssuanceID"] = strHex(key); + output.cftIssuances.push_back(jsonObj); + } + + return output; +} + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, CFTsByIssuerHandler::Output const& output) +{ + jv = { + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(validated), output.validated}, + {JS(issuer), output.issuer}, + {"cft_issuances", output.cftIssuances}, //todo:: add translation + }; + + if (output.marker) + jv.as_object()[JS(marker)] = boost::json::value_from(*(output.marker)); + + if (output.limit) + jv.as_object()[JS(limit)] = *(output.limit); +} + +CFTsByIssuerHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + auto input = CFTsByIssuerHandler::Input{}; + auto const& jsonObject = jv.as_object(); + + 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()); + else + // could not get the latest validated ledger seq here, using this flag to indicate that + input.usingValidatedLedger = true; + } + + if (jsonObject.contains(JS(limit))) + input.limit = jsonObject.at(JS(limit)).as_int64(); + + if (jsonObject.contains(JS(marker))) + input.marker = jsonObject.at(JS(marker)).as_string().c_str();; + + if (jsonObject.contains("include_deleted")) + input.includeDeleted = jsonObject.at("include_deleted").as_bool(); + + return input; +} + +} // namespace rpc diff --git a/src/rpc/handlers/CFTsByIssuer.h b/src/rpc/handlers/CFTsByIssuer.h new file mode 100644 index 000000000..43557c1a3 --- /dev/null +++ b/src/rpc/handlers/CFTsByIssuer.h @@ -0,0 +1,109 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include + +namespace rpc { + +/** + * @brief The nft_history command asks the Clio server for past transaction metadata for the NFT being queried. + * + * For more details see: https://xrpl.org/nft_history.html#nft_history + */ +class CFTsByIssuerHandler +{ + util::Logger log_{"RPC"}; + std::shared_ptr sharedPtrBackend_; + +public: + static auto constexpr LIMIT_MIN = 1; + static auto constexpr LIMIT_MAX = 100; + static auto constexpr LIMIT_DEFAULT = 50; + + struct Output + { + std::string issuer; + std::string ledgerHash; + uint32_t ledgerIndex; + std::optional limit; + std::optional marker; + // TODO: use a better type than json + boost::json::array cftIssuances; + // validated should be sent via framework + bool validated = true; + }; + + struct Input + { + std::string issuer; + // You must use at least one of the following fields in your request: + // ledger_index, ledger_hash + std::optional ledgerHash; + std::optional ledgerIndex; + std::optional limit; + std::optional marker; + std::optional includeDeleted; + bool usingValidatedLedger = false; + }; + + using Result = HandlerReturnType; + + CFTsByIssuerHandler(std::shared_ptr const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec([[maybe_unused]] uint32_t apiVersion) const + { + static auto const rpcSpec = RpcSpec{ + {JS(issuer), validation::Required{}, validation::AccountValidator}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(limit), + validation::Type{}, + validation::Min(1u), + modifiers::Clamp{LIMIT_MIN, LIMIT_MAX}}, + {JS(marker), validation::Uint256HexStringValidator}, + {"include_deleted", validation::Type{}} + + }; + + 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, boost::json::value const& jv); +}; + +} // namespace rpc diff --git a/unittests/util/MockBackend.h b/unittests/util/MockBackend.h index 56aa41f77..7d0d69f4d 100644 --- a/unittests/util/MockBackend.h +++ b/unittests/util/MockBackend.h @@ -149,4 +149,14 @@ struct MockBackend : public BackendInterface MOCK_METHOD(void, doWriteLedgerObject, (std::string&&, std::uint32_t const, std::string&&), (override)); MOCK_METHOD(bool, doFinishWrites, (), (override)); + + MOCK_METHOD((std::vector>>), doFetchLedgerObjectsPair, (std::vector const&, std::uint32_t const, boost::asio::yield_context), (const, override)); + + MOCK_METHOD(void, writeCFTIssuancePairs, ((std::vector>&&)), (override)); + + MOCK_METHOD(CFTIssuancesAndCursor, fetchIssuerCFTs, (ripple::AccountID const& issuer, + std::uint32_t const , + (std::optional const&) , + std::uint32_t const , + boost::asio::yield_context), (const, override)); };