diff --git a/CMakeLists.txt b/CMakeLists.txt index 960f9eb4..bab78f03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,7 +64,8 @@ target_sources(reporting PRIVATE handlers/Tx.cpp handlers/RPCHelpers.cpp handlers/AccountTx.cpp - handlers/LedgerData.cpp) + handlers/LedgerData.cpp + handlers/BookOffers.cpp) message(${Boost_LIBRARIES}) diff --git a/handlers/BookOffers.cpp b/handlers/BookOffers.cpp new file mode 100644 index 00000000..8e2189e5 --- /dev/null +++ b/handlers/BookOffers.cpp @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::optional +ledgerSequenceFromRequest( + boost::json::object const& request, + std::shared_ptr const& pool) +{ + std::stringstream sql; + sql << "SELECT ledger_seq FROM ledgers WHERE "; + + if (request.contains("ledger_index")) + { + sql << "ledger_seq = " + << std::to_string(request.at("ledger_index").as_uint64()); + } + else if (request.contains("ledger_hash")) + { + sql << "ledger_hash = \\\\x" << request.at("ledger_hash").as_string(); + } + else + { + sql.str(""); + sql << "SELECT max_ledger()"; + } + + sql << ";"; + + auto index = PgQuery(pool)(sql.str().c_str()); + if (!index || index.isNull()) + return {}; + + return std::optional{index.asInt()}; +} + +std::vector +loadBookOfferIndexes( + ripple::Book const& book, + std::uint32_t seq, + std::uint32_t limit, + std::shared_ptr const& pool) +{ + std::vector hashes = {}; + + ripple::uint256 bookBase = getBookBase(book); + ripple::uint256 bookEnd = getQualityNext(bookBase); + + pg_params dbParams; + + char const*& command = dbParams.first; + std::vector>& values = dbParams.second; + + command = + "SELECT offer_indexes FROM books " + "WHERE book_directory >= $1::bytea " + "AND book_directory < $2::bytea " + "AND ledger_index <= $3::bigint " + "LIMIT $4::bigint"; + + values.resize(4); + values[0] = "\\x" + ripple::strHex(bookBase); + values[1] = "\\x" + ripple::strHex(bookEnd); + values[2] = std::to_string(seq); + values[3] = std::to_string(limit); + + auto indexes = PgQuery(pool)(dbParams); + if (!indexes || indexes.isNull()) + return {}; + + for (auto i = 0; i < indexes.ntuples(); ++i) + { + auto unHexed = ripple::strUnHex(indexes.c_str(i) + 2); + if (unHexed) + hashes.push_back(ripple::uint256::fromVoid(unHexed->data())); + } + + return hashes; +} + +boost::json::object +doBookOffers( + boost::json::object const& request, + CassandraFlatMapBackend const& backend, + std::shared_ptr& pool) +{ + boost::json::object response; + auto sequence = ledgerSequenceFromRequest(request, pool); + + if (!sequence) + return response; + + if (!request.contains("taker_pays")) + { + response["error"] = "Missing field taker_pays"; + return response; + } + + if (!request.contains("taker_gets")) + { + response["error"] = "Missing field taker_gets"; + return response; + } + + boost::json::object taker_pays; + if (request.at("taker_pays").kind() == boost::json::kind::object) + { + taker_pays = request.at("taker_pays").as_object(); + } + else + { + response["error"] = "Invalid field taker_pays"; + return response; + } + + boost::json::object taker_gets; + if (request.at("taker_gets").kind() == boost::json::kind::object) + { + taker_gets = request.at("taker_gets").as_object(); + } + else + { + response["error"] = "Invalid field taker_gets"; + return response; + } + + if (!taker_pays.contains("currency")) + { + response["error"] = "Missing field taker_pays.currency"; + return response; + } + + if (!taker_pays.at("currency").is_string()) + { + response["error"] = "taker_pays.currency should be string"; + return response; + } + + if (!taker_gets.contains("currency")) + { + response["error"] = "Missing field taker_gets.currency"; + return response; + } + + if (!taker_gets.at("currency").is_string()) + { + response["error"] = "taker_gets.currency should be string"; + return response; + } + + ripple::Currency pay_currency; + + if (!ripple::to_currency( + pay_currency, taker_pays.at("currency").as_string().c_str())) + { + response["error"] = + "Invalid field 'taker_pays.currency', bad currency."; + return response; + } + + ripple::Currency get_currency; + + if (!ripple::to_currency( + get_currency, taker_gets["currency"].as_string().c_str())) + { + response["error"] = + "Invalid field 'taker_gets.currency', bad currency."; + return response; + } + + ripple::AccountID pay_issuer; + + if (taker_pays.contains("issuer")) + { + if (!taker_pays.at("issuer").is_string()) + { + response["error"] = "taker_pays.issuer should be string"; + return response; + } + + if (!ripple::to_issuer( + pay_issuer, taker_pays.at("issuer").as_string().c_str())) + { + response["error"] = + "Invalid field 'taker_pays.issuer', bad issuer."; + return response; + } + + if (pay_issuer == ripple::noAccount()) + { + response["error"] = + "Invalid field 'taker_pays.issuer', bad issuer account one."; + return response; + } + } + else + { + pay_issuer = ripple::xrpAccount(); + } + + if (isXRP(pay_currency) && !isXRP(pay_issuer)) + { + response["error"] = + "Unneeded field 'taker_pays.issuer' for XRP currency " + "specification."; + return response; + } + + if (!isXRP(pay_currency) && isXRP(pay_issuer)) + { + response["error"] = + "Invalid field 'taker_pays.issuer', expected non-XRP issuer."; + return response; + } + + ripple::AccountID get_issuer; + + if (taker_gets.contains("issuer")) + { + if (!taker_gets["issuer"].is_string()) + { + response["error"] = "taker_gets.issuer should be string"; + return response; + } + + if (!ripple::to_issuer( + get_issuer, taker_gets.at("issuer").as_string().c_str())) + { + response["error"] = + "Invalid field 'taker_gets.issuer', bad issuer."; + return response; + } + + if (get_issuer == ripple::noAccount()) + { + response["error"] = + "Invalid field 'taker_gets.issuer', bad issuer account one."; + return response; + } + } + else + { + get_issuer = ripple::xrpAccount(); + } + + if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer)) + { + response["error"] = + "Unneeded field 'taker_gets.issuer' for XRP currency " + "specification."; + return response; + } + + if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer)) + { + response["error"] = + "Invalid field 'taker_gets.issuer', expected non-XRP issuer."; + return response; + } + + boost::optional takerID; + if (request.contains("taker")) + { + if (!request.at("taker").is_string()) + { + response["error"] = "taker should be string"; + return response; + } + + takerID = ripple::parseBase58( + request.at("taker").as_string().c_str()); + if (!takerID) + { + response["error"] = "Invalid taker"; + return response; + } + } + + if (pay_currency == get_currency && pay_issuer == get_issuer) + { + response["error"] = "Bad market"; + return response; + } + + std::uint32_t limit = 2048; + if (request.at("limit").kind() == boost::json::kind::int64) + limit = request.at("limit").as_int64(); + + ripple::Book book = { + {pay_currency, pay_issuer}, {get_currency, get_issuer}}; + + auto start = std::chrono::system_clock::now(); + ripple::uint256 bookBase = getBookBase(book); + std::vector offers = + backend.doBookOffers(bookBase, *sequence); + auto end = std::chrono::system_clock::now(); + + BOOST_LOG_TRIVIAL(warning) << "Time loading books from Postgres: " + << ((end - start).count() / 1000000000.0); + + response["offers"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonOffers = response.at("offers").as_array(); + + start = std::chrono::system_clock::now(); + std::transform( + std::move_iterator(offers.begin()), + std::move_iterator(offers.end()), + std::back_inserter(jsonOffers), + [](auto obj) { + ripple::SerialIter it{obj.blob.data(), obj.blob.size()}; + ripple::SLE offer{it, obj.key}; + return getJson(offer); + }); + + end = std::chrono::system_clock::now(); + + BOOST_LOG_TRIVIAL(warning) << "Time transforming to json: " + << ((end - start).count() / 1000000000.0); + + return response; +} diff --git a/reporting/ReportingBackend.h b/reporting/ReportingBackend.h index 6fcc5c09..60f6a0ce 100644 --- a/reporting/ReportingBackend.h +++ b/reporting/ReportingBackend.h @@ -1206,8 +1206,7 @@ public: } std::vector - doBookOffers(std::vector const& book, uint32_t sequence) - const + doBookOffers(ripple::uint256 const& book, uint32_t sequence) const { BOOST_LOG_TRIVIAL(debug) << "Starting doBookOffers"; CassStatement* statement = cass_prepared_bind(upperBound_);