mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-20 11:45:53 +00:00
@@ -169,6 +169,7 @@ if(BUILD_TESTS)
|
||||
unittests/rpc/handlers/NFTBuyOffersTest.cpp
|
||||
unittests/rpc/handlers/NFTSellOffersTest.cpp
|
||||
unittests/rpc/handlers/NFTHistoryTest.cpp
|
||||
unittests/rpc/handlers/SubscribeTest.cpp
|
||||
# Backend
|
||||
unittests/backend/cassandra/BaseTests.cpp
|
||||
unittests/backend/cassandra/BackendTests.cpp
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
class WsBase;
|
||||
class SubscriptionManager;
|
||||
namespace RPCng {
|
||||
|
||||
/**
|
||||
@@ -64,7 +65,7 @@ struct Context
|
||||
// TODO: we shall change yield_context to const yield_context after we
|
||||
// update backend interfaces to use const& yield
|
||||
std::reference_wrapper<boost::asio::yield_context> yield;
|
||||
std::shared_ptr<WsBase const> session;
|
||||
std::shared_ptr<WsBase> session;
|
||||
bool isAdmin = false;
|
||||
std::string clientIp;
|
||||
};
|
||||
|
||||
@@ -134,7 +134,7 @@ CustomValidator AccountValidator =
|
||||
// remove all old handler, this function can be moved to here
|
||||
if (!RPC::accountFromStringStrict(value.as_string().c_str()))
|
||||
{
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
return Error{RPC::Status{RPC::RippledError::rpcACT_MALFORMED, std::string(key) + "Malformed"}};
|
||||
}
|
||||
return MaybeError{};
|
||||
}};
|
||||
@@ -199,4 +199,71 @@ CustomValidator IssuerValidator =
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator SubscribeStreamValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
static std::unordered_set<std::string> const validStreams = {
|
||||
"ledger", "transactions", "transactions_proposed", "book_changes", "manifests", "validations"};
|
||||
if (!value.is_array())
|
||||
{
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
|
||||
}
|
||||
for (auto const& v : value.as_array())
|
||||
{
|
||||
if (!v.is_string())
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "streamNotString"}};
|
||||
if (not validStreams.contains(v.as_string().c_str()))
|
||||
return Error{RPC::Status{RPC::RippledError::rpcSTREAM_MALFORMED}};
|
||||
}
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator SubscribeAccountsValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_array())
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
|
||||
if (value.as_array().size() == 0)
|
||||
return Error{RPC::Status{RPC::RippledError::rpcACT_MALFORMED, std::string(key) + " malformed."}};
|
||||
|
||||
for (auto const& v : value.as_array())
|
||||
{
|
||||
auto obj = boost::json::object();
|
||||
auto const keyItem = std::string(key) + "'sItem";
|
||||
obj[keyItem] = v;
|
||||
if (auto const err = AccountValidator.verify(obj, keyItem); !err)
|
||||
return err;
|
||||
}
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator SubscribeBooksValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_array())
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotArray"}};
|
||||
for (auto const& book : value.as_array())
|
||||
{
|
||||
if (!book.is_object())
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "ItemNotObject"}};
|
||||
if (book.as_object().contains("both") && !book.as_object().at("both").is_bool())
|
||||
{
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "bothNotBool"}};
|
||||
}
|
||||
|
||||
if (book.as_object().contains("snapshot") && !book.as_object().at("snapshot").is_bool())
|
||||
{
|
||||
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "snapshotNotBool"}};
|
||||
}
|
||||
|
||||
if (book.as_object().contains("taker"))
|
||||
{
|
||||
if (auto const err = AccountValidator.verify(book.as_object(), "taker"); !err)
|
||||
return err;
|
||||
}
|
||||
|
||||
auto const parsedBook = RPC::parseBook(book.as_object());
|
||||
if (auto const status = std::get_if<RPC::Status>(&parsedBook))
|
||||
return Error(*status);
|
||||
}
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace RPCng::validation
|
||||
|
||||
@@ -482,4 +482,22 @@ extern CustomValidator CurrencyValidator;
|
||||
*/
|
||||
extern CustomValidator IssuerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a validator for validating valid streams used in
|
||||
* subscribe/unsubscribe
|
||||
*/
|
||||
extern CustomValidator SubscribeStreamValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a validator for validating valid accounts used in
|
||||
* subscribe/unsubscribe
|
||||
*/
|
||||
extern CustomValidator SubscribeAccountsValidator;
|
||||
|
||||
/**
|
||||
* @brief Provide a validator for validating valid books used in
|
||||
* subscribe/unsubscribe
|
||||
*/
|
||||
extern CustomValidator SubscribeBooksValidator;
|
||||
|
||||
} // namespace RPCng::validation
|
||||
|
||||
248
src/rpc/ngHandlers/Subscribe.h
Normal file
248
src/rpc/ngHandlers/Subscribe.h
Normal file
@@ -0,0 +1,248 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <backend/BackendInterface.h>
|
||||
#include <rpc/RPCHelpers.h>
|
||||
#include <rpc/common/Types.h>
|
||||
#include <rpc/common/Validators.h>
|
||||
|
||||
namespace RPCng {
|
||||
template <typename SubscriptionManagerType>
|
||||
class BaseSubscribeHandler
|
||||
{
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
std::shared_ptr<SubscriptionManagerType> subscriptions_;
|
||||
|
||||
public:
|
||||
struct Output
|
||||
{
|
||||
// response of stream "ledger"
|
||||
// TODO: use better type than json, this type will be used in the stream as well
|
||||
std::optional<boost::json::object> ledger;
|
||||
// books returns nothing by default, if snapshot is true, it returns offers
|
||||
// TODO: use better type than json
|
||||
std::optional<boost::json::array> offers;
|
||||
bool validated = true;
|
||||
};
|
||||
|
||||
struct OrderBook
|
||||
{
|
||||
ripple::Book book;
|
||||
std::optional<std::string> taker;
|
||||
bool snapshot = false;
|
||||
bool both = false;
|
||||
};
|
||||
|
||||
struct Input
|
||||
{
|
||||
std::optional<std::vector<std::string>> accounts;
|
||||
std::optional<std::vector<std::string>> streams;
|
||||
std::optional<std::vector<std::string>> accountsProposed;
|
||||
std::optional<std::vector<OrderBook>> books;
|
||||
};
|
||||
|
||||
using Result = RPCng::HandlerReturnType<Output>;
|
||||
|
||||
BaseSubscribeHandler(
|
||||
std::shared_ptr<BackendInterface> const& sharedPtrBackend,
|
||||
std::shared_ptr<SubscriptionManagerType> const& subscriptions)
|
||||
: sharedPtrBackend_(sharedPtrBackend), subscriptions_(subscriptions)
|
||||
{
|
||||
}
|
||||
|
||||
RpcSpecConstRef
|
||||
spec() const
|
||||
{
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(streams), validation::SubscribeStreamValidator},
|
||||
{JS(accounts), validation::SubscribeAccountsValidator},
|
||||
{JS(accounts_proposed), validation::SubscribeAccountsValidator},
|
||||
{JS(books), validation::SubscribeBooksValidator}};
|
||||
return rpcSpec;
|
||||
}
|
||||
|
||||
Result
|
||||
process(Input input, Context const& ctx) const
|
||||
{
|
||||
Output output;
|
||||
if (input.streams)
|
||||
{
|
||||
auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
|
||||
if (!ledger.empty())
|
||||
output.ledger = ledger;
|
||||
}
|
||||
if (input.accounts)
|
||||
subscribeToAccounts(*(input.accounts), ctx.session);
|
||||
if (input.accountsProposed)
|
||||
subscribeToAccountsProposed(*(input.accountsProposed), ctx.session);
|
||||
if (input.books)
|
||||
{
|
||||
auto const offers = subscribeToBooks(*(input.books), ctx.session, ctx.yield);
|
||||
if (!offers.empty())
|
||||
output.offers = offers;
|
||||
};
|
||||
return output;
|
||||
}
|
||||
|
||||
private:
|
||||
boost::json::object
|
||||
subscribeToStreams(
|
||||
boost::asio::yield_context& yield,
|
||||
std::vector<std::string> const& streams,
|
||||
std::shared_ptr<WsBase> const& session) const
|
||||
{
|
||||
boost::json::object response;
|
||||
for (auto const& stream : streams)
|
||||
{
|
||||
if (stream == "ledger")
|
||||
response = subscriptions_->subLedger(yield, session);
|
||||
else if (stream == "transactions")
|
||||
subscriptions_->subTransactions(session);
|
||||
else if (stream == "transactions_proposed")
|
||||
subscriptions_->subProposedTransactions(session);
|
||||
else if (stream == "validations")
|
||||
subscriptions_->subValidation(session);
|
||||
else if (stream == "manifests")
|
||||
subscriptions_->subManifest(session);
|
||||
else if (stream == "book_changes")
|
||||
subscriptions_->subBookChanges(session);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void
|
||||
subscribeToAccounts(std::vector<std::string> const& accounts, std::shared_ptr<WsBase> const& session) const
|
||||
{
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
auto const accountID = RPC::accountFromStringStrict(account);
|
||||
subscriptions_->subAccount(*accountID, session);
|
||||
}
|
||||
}
|
||||
void
|
||||
subscribeToAccountsProposed(std::vector<std::string> const& accounts, std::shared_ptr<WsBase> const& session) const
|
||||
{
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
auto const accountID = RPC::accountFromStringStrict(account);
|
||||
subscriptions_->subProposedAccount(*accountID, session);
|
||||
}
|
||||
}
|
||||
|
||||
boost::json::array
|
||||
subscribeToBooks(
|
||||
std::vector<OrderBook> const& books,
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
boost::asio::yield_context& yield) const
|
||||
{
|
||||
boost::json::array snapshots;
|
||||
std::optional<Backend::LedgerRange> rng;
|
||||
static auto constexpr fetchLimit = 200;
|
||||
|
||||
for (auto const& internalBook : books)
|
||||
{
|
||||
if (internalBook.snapshot)
|
||||
{
|
||||
if (!rng)
|
||||
rng = sharedPtrBackend_->fetchLedgerRange();
|
||||
auto const getOrderBook = [&](auto const& book) {
|
||||
auto const bookBase = getBookBase(book);
|
||||
auto const [offers, _] =
|
||||
sharedPtrBackend_->fetchBookOffers(bookBase, rng->maxSequence, fetchLimit, yield);
|
||||
|
||||
// the taker is not really uesed, same issue with
|
||||
// https://github.com/XRPLF/xrpl-dev-portal/issues/1818
|
||||
auto const takerID =
|
||||
internalBook.taker ? RPC::accountFromStringStrict(*(internalBook.taker)) : beast::zero;
|
||||
|
||||
auto const orderBook =
|
||||
RPC::postProcessOrderBook(offers, book, *takerID, *sharedPtrBackend_, rng->maxSequence, yield);
|
||||
std::copy(orderBook.begin(), orderBook.end(), std::back_inserter(snapshots));
|
||||
};
|
||||
getOrderBook(internalBook.book);
|
||||
if (internalBook.both)
|
||||
getOrderBook(ripple::reversed(internalBook.book));
|
||||
}
|
||||
|
||||
subscriptions_->subBook(internalBook.book, session);
|
||||
if (internalBook.both)
|
||||
subscriptions_->subBook(ripple::reversed(internalBook.book), session);
|
||||
}
|
||||
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
friend void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output)
|
||||
{
|
||||
jv = output.ledger ? *(output.ledger) : boost::json::object();
|
||||
if (output.offers)
|
||||
jv.as_object().emplace(JS(offers), *(output.offers));
|
||||
}
|
||||
|
||||
friend Input
|
||||
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv)
|
||||
{
|
||||
auto const& jsonObject = jv.as_object();
|
||||
Input input;
|
||||
if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end())
|
||||
{
|
||||
input.streams = std::vector<std::string>();
|
||||
for (auto const& stream : streams->value().as_array())
|
||||
input.streams->push_back(stream.as_string().c_str());
|
||||
}
|
||||
if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end())
|
||||
{
|
||||
input.accounts = std::vector<std::string>();
|
||||
for (auto const& account : accounts->value().as_array())
|
||||
input.accounts->push_back(account.as_string().c_str());
|
||||
}
|
||||
if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); accountsProposed != jsonObject.end())
|
||||
{
|
||||
input.accountsProposed = std::vector<std::string>();
|
||||
for (auto const& account : accountsProposed->value().as_array())
|
||||
input.accountsProposed->push_back(account.as_string().c_str());
|
||||
}
|
||||
if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end())
|
||||
{
|
||||
input.books = std::vector<OrderBook>();
|
||||
for (auto const& book : books->value().as_array())
|
||||
{
|
||||
auto const& bookObject = book.as_object();
|
||||
OrderBook internalBook;
|
||||
if (auto const& taker = bookObject.find(JS(taker)); taker != bookObject.end())
|
||||
internalBook.taker = taker->value().as_string().c_str();
|
||||
if (auto const& both = bookObject.find(JS(both)); both != bookObject.end())
|
||||
internalBook.both = both->value().as_bool();
|
||||
if (auto const& snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end())
|
||||
internalBook.snapshot = snapshot->value().as_bool();
|
||||
auto const parsedBookMaybe = RPC::parseBook(book.as_object());
|
||||
internalBook.book = std::get<ripple::Book>(parsedBookMaybe);
|
||||
input.books->push_back(internalBook);
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
using SubscribeHandler = BaseSubscribeHandler<SubscriptionManager>;
|
||||
|
||||
} // namespace RPCng
|
||||
@@ -66,7 +66,7 @@ getLedgerPubMessage(
|
||||
boost::json::object
|
||||
SubscriptionManager::subLedger(boost::asio::yield_context& yield, std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, ledgerSubscribers_, [this](session_ptr session) { unsubLedger(session); });
|
||||
subscribeHelper(session, ledgerSubscribers_, [this](SessionPtrType session) { unsubLedger(session); });
|
||||
|
||||
auto ledgerRange = backend_->fetchLedgerRange();
|
||||
assert(ledgerRange);
|
||||
@@ -94,7 +94,7 @@ SubscriptionManager::unsubLedger(std::shared_ptr<WsBase> session)
|
||||
void
|
||||
SubscriptionManager::subTransactions(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, txSubscribers_, [this](session_ptr session) { unsubTransactions(session); });
|
||||
subscribeHelper(session, txSubscribers_, [this](SessionPtrType session) { unsubTransactions(session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -104,15 +104,15 @@ SubscriptionManager::unsubTransactions(std::shared_ptr<WsBase> session)
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionManager::subAccount(ripple::AccountID const& account, std::shared_ptr<WsBase>& session)
|
||||
SubscriptionManager::subAccount(ripple::AccountID const& account, std::shared_ptr<WsBase> const& session)
|
||||
{
|
||||
subscribeHelper(session, account, accountSubscribers_, [this, account](session_ptr session) {
|
||||
subscribeHelper(session, account, accountSubscribers_, [this, account](SessionPtrType session) {
|
||||
unsubAccount(account, session);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionManager::unsubAccount(ripple::AccountID const& account, std::shared_ptr<WsBase>& session)
|
||||
SubscriptionManager::unsubAccount(ripple::AccountID const& account, std::shared_ptr<WsBase> const& session)
|
||||
{
|
||||
accountSubscribers_.unsubscribe(session, account);
|
||||
}
|
||||
@@ -120,7 +120,8 @@ SubscriptionManager::unsubAccount(ripple::AccountID const& account, std::shared_
|
||||
void
|
||||
SubscriptionManager::subBook(ripple::Book const& book, std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, book, bookSubscribers_, [this, book](session_ptr session) { unsubBook(book, session); });
|
||||
subscribeHelper(
|
||||
session, book, bookSubscribers_, [this, book](SessionPtrType session) { unsubBook(book, session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -132,7 +133,7 @@ SubscriptionManager::unsubBook(ripple::Book const& book, std::shared_ptr<WsBase>
|
||||
void
|
||||
SubscriptionManager::subBookChanges(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, bookChangesSubscribers_, [this](session_ptr session) { unsubBookChanges(session); });
|
||||
subscribeHelper(session, bookChangesSubscribers_, [this](SessionPtrType session) { unsubBookChanges(session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -281,7 +282,7 @@ SubscriptionManager::forwardValidation(boost::json::object const& response)
|
||||
void
|
||||
SubscriptionManager::subProposedAccount(ripple::AccountID const& account, std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, account, accountProposedSubscribers_, [this, account](session_ptr session) {
|
||||
subscribeHelper(session, account, accountProposedSubscribers_, [this, account](SessionPtrType session) {
|
||||
unsubProposedAccount(account, session);
|
||||
});
|
||||
}
|
||||
@@ -289,7 +290,7 @@ SubscriptionManager::subProposedAccount(ripple::AccountID const& account, std::s
|
||||
void
|
||||
SubscriptionManager::subManifest(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, manifestSubscribers_, [this](session_ptr session) { unsubManifest(session); });
|
||||
subscribeHelper(session, manifestSubscribers_, [this](SessionPtrType session) { unsubManifest(session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -301,7 +302,7 @@ SubscriptionManager::unsubManifest(std::shared_ptr<WsBase> session)
|
||||
void
|
||||
SubscriptionManager::subValidation(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(session, validationsSubscribers_, [this](session_ptr session) { unsubValidation(session); });
|
||||
subscribeHelper(session, validationsSubscribers_, [this](SessionPtrType session) { unsubValidation(session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -320,7 +321,7 @@ void
|
||||
SubscriptionManager::subProposedTransactions(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
subscribeHelper(
|
||||
session, txProposedSubscribers_, [this](session_ptr session) { unsubProposedTransactions(session); });
|
||||
session, txProposedSubscribers_, [this](SessionPtrType session) { unsubProposedTransactions(session); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -328,17 +329,19 @@ SubscriptionManager::unsubProposedTransactions(std::shared_ptr<WsBase> session)
|
||||
{
|
||||
txProposedSubscribers_.unsubscribe(session);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionManager::subscribeHelper(std::shared_ptr<WsBase>& session, Subscription& subs, CleanupFunction&& func)
|
||||
SubscriptionManager::subscribeHelper(std::shared_ptr<WsBase> const& session, Subscription& subs, CleanupFunction&& func)
|
||||
{
|
||||
subs.subscribe(session);
|
||||
std::scoped_lock lk(cleanupMtx_);
|
||||
cleanupFuncs_[session].push_back(std::move(func));
|
||||
}
|
||||
|
||||
template <typename Key>
|
||||
void
|
||||
SubscriptionManager::subscribeHelper(
|
||||
std::shared_ptr<WsBase>& session,
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
Key const& k,
|
||||
SubscriptionMap<Key>& subs,
|
||||
CleanupFunction&& func)
|
||||
|
||||
@@ -189,7 +189,7 @@ SubscriptionMap<Key>::publish(std::shared_ptr<Message> const& message, Key const
|
||||
|
||||
class SubscriptionManager
|
||||
{
|
||||
using session_ptr = std::shared_ptr<WsBase>;
|
||||
using SessionPtrType = std::shared_ptr<WsBase>;
|
||||
clio::Logger log_{"Subscriptions"};
|
||||
|
||||
std::vector<std::thread> workers_;
|
||||
@@ -251,7 +251,7 @@ public:
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
subLedger(boost::asio::yield_context& yield, session_ptr session);
|
||||
subLedger(boost::asio::yield_context& yield, SessionPtrType session);
|
||||
|
||||
void
|
||||
pubLedger(
|
||||
@@ -264,28 +264,28 @@ public:
|
||||
pubBookChanges(ripple::LedgerInfo const& lgrInfo, std::vector<Backend::TransactionAndMetadata> const& transactions);
|
||||
|
||||
void
|
||||
unsubLedger(session_ptr session);
|
||||
unsubLedger(SessionPtrType session);
|
||||
|
||||
void
|
||||
subTransactions(session_ptr session);
|
||||
subTransactions(SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubTransactions(session_ptr session);
|
||||
unsubTransactions(SessionPtrType session);
|
||||
|
||||
void
|
||||
pubTransaction(Backend::TransactionAndMetadata const& blobs, ripple::LedgerInfo const& lgrInfo);
|
||||
|
||||
void
|
||||
subAccount(ripple::AccountID const& account, session_ptr& session);
|
||||
subAccount(ripple::AccountID const& account, SessionPtrType const& session);
|
||||
|
||||
void
|
||||
unsubAccount(ripple::AccountID const& account, session_ptr& session);
|
||||
unsubAccount(ripple::AccountID const& account, SessionPtrType const& session);
|
||||
|
||||
void
|
||||
subBook(ripple::Book const& book, session_ptr session);
|
||||
subBook(ripple::Book const& book, SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubBook(ripple::Book const& book, session_ptr session);
|
||||
unsubBook(ripple::Book const& book, SessionPtrType session);
|
||||
|
||||
void
|
||||
subBookChanges(std::shared_ptr<WsBase> session);
|
||||
@@ -294,16 +294,16 @@ public:
|
||||
unsubBookChanges(std::shared_ptr<WsBase> session);
|
||||
|
||||
void
|
||||
subManifest(session_ptr session);
|
||||
subManifest(SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubManifest(session_ptr session);
|
||||
unsubManifest(SessionPtrType session);
|
||||
|
||||
void
|
||||
subValidation(session_ptr session);
|
||||
subValidation(SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubValidation(session_ptr session);
|
||||
unsubValidation(SessionPtrType session);
|
||||
|
||||
void
|
||||
forwardProposedTransaction(boost::json::object const& response);
|
||||
@@ -315,19 +315,19 @@ public:
|
||||
forwardValidation(boost::json::object const& response);
|
||||
|
||||
void
|
||||
subProposedAccount(ripple::AccountID const& account, session_ptr session);
|
||||
subProposedAccount(ripple::AccountID const& account, SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubProposedAccount(ripple::AccountID const& account, session_ptr session);
|
||||
unsubProposedAccount(ripple::AccountID const& account, SessionPtrType session);
|
||||
|
||||
void
|
||||
subProposedTransactions(session_ptr session);
|
||||
subProposedTransactions(SessionPtrType session);
|
||||
|
||||
void
|
||||
unsubProposedTransactions(session_ptr session);
|
||||
unsubProposedTransactions(SessionPtrType session);
|
||||
|
||||
void
|
||||
cleanup(session_ptr session);
|
||||
cleanup(SessionPtrType session);
|
||||
|
||||
boost::json::object
|
||||
report() const
|
||||
@@ -349,16 +349,20 @@ public:
|
||||
|
||||
private:
|
||||
void
|
||||
sendAll(std::string const& pubMsg, std::unordered_set<session_ptr>& subs);
|
||||
sendAll(std::string const& pubMsg, std::unordered_set<SessionPtrType>& subs);
|
||||
|
||||
using CleanupFunction = std::function<void(session_ptr)>;
|
||||
using CleanupFunction = std::function<void(SessionPtrType const)>;
|
||||
|
||||
void
|
||||
subscribeHelper(std::shared_ptr<WsBase>& session, Subscription& subs, CleanupFunction&& func);
|
||||
subscribeHelper(std::shared_ptr<WsBase> const& session, Subscription& subs, CleanupFunction&& func);
|
||||
|
||||
template <typename Key>
|
||||
void
|
||||
subscribeHelper(std::shared_ptr<WsBase>& session, Key const& k, SubscriptionMap<Key>& subs, CleanupFunction&& func);
|
||||
subscribeHelper(
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
Key const& k,
|
||||
SubscriptionMap<Key>& subs,
|
||||
CleanupFunction&& func);
|
||||
|
||||
/**
|
||||
* This is how we chose to cleanup subscriptions that have been closed.
|
||||
@@ -367,5 +371,5 @@ private:
|
||||
* closed.
|
||||
*/
|
||||
std::mutex cleanupMtx_;
|
||||
std::unordered_map<session_ptr, std::vector<CleanupFunction>> cleanupFuncs_ = {};
|
||||
std::unordered_map<SessionPtrType, std::vector<CleanupFunction>> cleanupFuncs_ = {};
|
||||
};
|
||||
|
||||
@@ -440,3 +440,94 @@ TEST_F(RPCBaseTest, IssuerValidator)
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, SubscribeStreamValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"streams", SubscribeStreamValidator}};
|
||||
auto passingInput = json::parse(
|
||||
R"({
|
||||
"streams":
|
||||
[
|
||||
"ledger",
|
||||
"transactions_proposed",
|
||||
"validations",
|
||||
"transactions",
|
||||
"manifests",
|
||||
"transactions",
|
||||
"book_changes"
|
||||
]
|
||||
})");
|
||||
ASSERT_TRUE(spec.validate(passingInput));
|
||||
|
||||
auto failingInput = json::parse(R"({ "streams": 256})");
|
||||
auto err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(R"({ "streams": ["test"]})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(R"({ "streams": [123]})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, SubscribeAccountsValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"accounts", SubscribeAccountsValidator}};
|
||||
auto passingInput =
|
||||
json::parse(R"({ "accounts": ["rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"]})");
|
||||
ASSERT_TRUE(spec.validate(passingInput));
|
||||
|
||||
auto failingInput = json::parse(R"({ "accounts": 256})");
|
||||
auto err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(R"({ "accounts": ["test"]})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(R"({ "accounts": [123]})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, SubscribeBooksValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"books", SubscribeBooksValidator}};
|
||||
auto passingInput = json::parse(
|
||||
R"({
|
||||
"books": [{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "USD",
|
||||
"issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq"
|
||||
}
|
||||
}]
|
||||
})");
|
||||
ASSERT_TRUE(spec.validate(passingInput));
|
||||
|
||||
auto failingInput = json::parse(R"({ "books": 256})");
|
||||
auto err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(R"({ "books": ["test"]})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
|
||||
failingInput = json::parse(
|
||||
R"({
|
||||
"books": [{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
}
|
||||
}]
|
||||
})");
|
||||
err = spec.validate(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ TEST_F(RPCAccountHandlerTest, AccountInvalidFormat)
|
||||
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").as_string(), "actMalformed");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "accountMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ generateTestValuesForParametersTest()
|
||||
return std::vector<AccountInfoParamTestCaseBundle>{
|
||||
AccountInfoParamTestCaseBundle{"MissingAccountAndIdent", R"({})", "actMalformed", "Account malformed."},
|
||||
AccountInfoParamTestCaseBundle{"AccountNotString", R"({"account":1})", "invalidParams", "accountNotString"},
|
||||
AccountInfoParamTestCaseBundle{"AccountInvalid", R"({"account":"xxx"})", "invalidParams", "accountMalformed"},
|
||||
AccountInfoParamTestCaseBundle{"AccountInvalid", R"({"account":"xxx"})", "actMalformed", "accountMalformed"},
|
||||
AccountInfoParamTestCaseBundle{"IdentNotString", R"({"ident":1})", "invalidParams", "identNotString"},
|
||||
AccountInfoParamTestCaseBundle{"IdentInvalid", R"({"ident":"xxx"})", "invalidParams", "identMalformed"},
|
||||
AccountInfoParamTestCaseBundle{"IdentInvalid", R"({"ident":"xxx"})", "actMalformed", "identMalformed"},
|
||||
AccountInfoParamTestCaseBundle{
|
||||
"SignerListsInvalid",
|
||||
R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "signer_lists":1})",
|
||||
|
||||
@@ -205,7 +205,7 @@ TEST_F(RPCAccountLinesHandlerTest, AccountInvalidFormat)
|
||||
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").as_string(), "actMalformed");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "accountMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ generateTestValuesForParametersTest()
|
||||
{
|
||||
"AccountInvalid",
|
||||
R"({"account": "123"})",
|
||||
"invalidParams",
|
||||
"actMalformed",
|
||||
"accountMalformed",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -255,7 +255,7 @@ generateParameterBookOffersTestBundles()
|
||||
},
|
||||
"taker": "123"
|
||||
})",
|
||||
"invalidParams",
|
||||
"actMalformed",
|
||||
"takerMalformed"},
|
||||
ParameterTestBundle{
|
||||
"TakerNotString",
|
||||
|
||||
@@ -98,7 +98,7 @@ generateParameterTestBundles()
|
||||
R"({
|
||||
"account": "1213"
|
||||
})",
|
||||
"invalidParams",
|
||||
"actMalformed",
|
||||
"accountMalformed"},
|
||||
ParameterTestBundle{
|
||||
"LedgerIndexInvalid",
|
||||
|
||||
@@ -88,7 +88,7 @@ generateTestValuesForParametersTest()
|
||||
"account": "123",
|
||||
"role": "gateway"
|
||||
})",
|
||||
"invalidParams",
|
||||
"actMalformed",
|
||||
"accountMalformed"},
|
||||
NoRippleParamTestCaseBundle{
|
||||
"InvalidRole",
|
||||
|
||||
885
unittests/rpc/handlers/SubscribeTest.cpp
Normal file
885
unittests/rpc/handlers/SubscribeTest.cpp
Normal file
@@ -0,0 +1,885 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/ngHandlers/Subscribe.h>
|
||||
#include <subscriptions/SubscriptionManager.h>
|
||||
#include <util/Fixtures.h>
|
||||
#include <util/MockWsBase.h>
|
||||
#include <util/TestObject.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace RPCng;
|
||||
namespace json = boost::json;
|
||||
using namespace testing;
|
||||
|
||||
constexpr static auto MINSEQ = 10;
|
||||
constexpr static auto MAXSEQ = 30;
|
||||
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
|
||||
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
|
||||
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
|
||||
constexpr static auto PAYS20USDGETS10XRPBOOKDIR = "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000";
|
||||
constexpr static auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000";
|
||||
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
|
||||
constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
|
||||
|
||||
class RPCSubscribeHandlerTest : public HandlerBaseTest
|
||||
{
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
HandlerBaseTest::SetUp();
|
||||
clio::Config cfg;
|
||||
subManager_ = SubscriptionManager::make_SubscriptionManager(cfg, mockBackendPtr);
|
||||
util::TagDecoratorFactory tagDecoratorFactory{cfg};
|
||||
session_ = std::make_shared<MockSession>(tagDecoratorFactory);
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
HandlerBaseTest::TearDown();
|
||||
}
|
||||
|
||||
std::shared_ptr<SubscriptionManager> subManager_;
|
||||
std::shared_ptr<WsBase> session_;
|
||||
};
|
||||
|
||||
struct SubscribeParamTestCaseBundle
|
||||
{
|
||||
std::string testName;
|
||||
std::string testJson;
|
||||
std::string expectedError;
|
||||
std::string expectedErrorMessage;
|
||||
};
|
||||
|
||||
// parameterized test cases for parameters check
|
||||
struct SubscribeParameterTest : public RPCSubscribeHandlerTest, public WithParamInterface<SubscribeParamTestCaseBundle>
|
||||
{
|
||||
struct NameGenerator
|
||||
{
|
||||
template <class ParamType>
|
||||
std::string
|
||||
operator()(const testing::TestParamInfo<ParamType>& info) const
|
||||
{
|
||||
auto bundle = static_cast<SubscribeParamTestCaseBundle>(info.param);
|
||||
return bundle.testName;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
static auto
|
||||
generateTestValuesForParametersTest()
|
||||
{
|
||||
return std::vector<SubscribeParamTestCaseBundle>{
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsNotArray",
|
||||
R"({"accounts": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"})",
|
||||
"invalidParams",
|
||||
"accountsNotArray"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsItemNotString", R"({"accounts": [123]})", "invalidParams", "accounts'sItemNotString"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsItemInvalidString", R"({"accounts": ["123"]})", "actMalformed", "accounts'sItemMalformed"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsEmptyArray", R"({"accounts": []})", "actMalformed", "accounts malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsProposedNotArray",
|
||||
R"({"accounts_proposed": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"})",
|
||||
"invalidParams",
|
||||
"accounts_proposedNotArray"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsProposedItemNotString",
|
||||
R"({"accounts_proposed": [123]})",
|
||||
"invalidParams",
|
||||
"accounts_proposed'sItemNotString"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsProposedItemInvalidString",
|
||||
R"({"accounts_proposed": ["123"]})",
|
||||
"actMalformed",
|
||||
"accounts_proposed'sItemMalformed"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"AccountsProposedEmptyArray",
|
||||
R"({"accounts_proposed": []})",
|
||||
"actMalformed",
|
||||
"accounts_proposed malformed."},
|
||||
SubscribeParamTestCaseBundle{"StreamsNotArray", R"({"streams": 1})", "invalidParams", "streamsNotArray"},
|
||||
SubscribeParamTestCaseBundle{"StreamNotString", R"({"streams": [1]})", "invalidParams", "streamNotString"},
|
||||
SubscribeParamTestCaseBundle{"StreamNotValid", R"({"streams": ["1"]})", "malformedStream", "Stream malformed."},
|
||||
SubscribeParamTestCaseBundle{"BooksNotArray", R"({"books": "1"})", "invalidParams", "booksNotArray"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemNotObject", R"({"books": ["1"]})", "invalidParams", "booksItemNotObject"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemMissingTakerPays",
|
||||
R"({"books": [{"taker_gets": {"currency": "XRP"}}]})",
|
||||
"invalidParams",
|
||||
"Missing field 'taker_pays'"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemMissingTakerGets",
|
||||
R"({"books": [{"taker_pays": {"currency": "XRP"}}]})",
|
||||
"invalidParams",
|
||||
"Missing field 'taker_gets'"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsNotObject",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": "USD"
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"Field 'taker_gets' is not an object"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysNotObject",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": "USD"
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"Field 'taker_pays' is not an object"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysMissingCurrency",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcCurMalformed",
|
||||
"Source currency is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsMissingCurrency",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstAmtMalformed",
|
||||
"Destination amount/currency/issuer is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysCurrencyNotString",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {
|
||||
"currency": 1,
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcCurMalformed",
|
||||
"Source currency is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsCurrencyNotString",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": 1,
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstAmtMalformed",
|
||||
"Destination amount/currency/issuer is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysInvalidCurrency",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {
|
||||
"currency": "XXXXXX",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcCurMalformed",
|
||||
"Source currency is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsInvalidCurrency",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "xxxxxxx",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstAmtMalformed",
|
||||
"Destination amount/currency/issuer is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysMissingIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcIsrMalformed",
|
||||
"Invalid field 'taker_pays.issuer', expected non-XRP issuer."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsMissingIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstIsrMalformed",
|
||||
"Invalid field 'taker_gets.issuer', expected non-XRP issuer."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysIssuerNotString",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {
|
||||
"currency": "USD",
|
||||
"issuer": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"takerPaysIssuerNotString"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsIssuerNotString",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"taker_gets.issuer should be string"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysInvalidIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_gets":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_pays": {
|
||||
"currency": "USD",
|
||||
"issuer": "123"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcIsrMalformed",
|
||||
"Source issuer is malformed."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsInvalidIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "123"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstIsrMalformed",
|
||||
"Invalid field 'taker_gets.issuer', bad issuer."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerGetsXRPHasIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "XRP",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"dstIsrMalformed",
|
||||
"Unneeded field 'taker_gets.issuer' for XRP currency "
|
||||
"specification."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemTakerPaysXRPHasIssuer",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"srcIsrMalformed",
|
||||
"Unneeded field 'taker_pays.issuer' for XRP currency "
|
||||
"specification."},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemBadMartket",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "XRP"
|
||||
}
|
||||
}
|
||||
]
|
||||
})",
|
||||
"badMarket",
|
||||
"badMarket"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemInvalidSnapshot",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"snapshot": 0
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"snapshotNotBool"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemInvalidBoth",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"both": 0
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"bothNotBool"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemInvalidTakerNotString",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"taker": 0
|
||||
}
|
||||
]
|
||||
})",
|
||||
"invalidParams",
|
||||
"takerNotString"},
|
||||
SubscribeParamTestCaseBundle{
|
||||
"BooksItemInvalidTaker",
|
||||
R"({
|
||||
"books": [
|
||||
{
|
||||
"taker_pays":
|
||||
{
|
||||
"currency": "XRP"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
|
||||
},
|
||||
"taker": "xxxxxxx"
|
||||
}
|
||||
]
|
||||
})",
|
||||
"actMalformed",
|
||||
"takerMalformed"},
|
||||
};
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
RPCSubscribe,
|
||||
SubscribeParameterTest,
|
||||
ValuesIn(generateTestValuesForParametersTest()),
|
||||
SubscribeParameterTest::NameGenerator{});
|
||||
|
||||
TEST_P(SubscribeParameterTest, InvalidParams)
|
||||
{
|
||||
auto const testBundle = GetParam();
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const req = json::parse(testBundle.testJson);
|
||||
auto const output = handler.process(req, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = RPC::makeError(output.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError);
|
||||
EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, EmptyResponse)
|
||||
{
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(json::parse(R"({})"), Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, StreamsWithoutLedger)
|
||||
{
|
||||
// these streams don't return response
|
||||
auto const input = json::parse(
|
||||
R"({
|
||||
"streams": ["transactions_proposed","transactions","validations","manifests","book_changes"]
|
||||
})");
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
EXPECT_EQ(report.at("transactions_proposed").as_uint64(), 1);
|
||||
EXPECT_EQ(report.at("transactions").as_uint64(), 1);
|
||||
EXPECT_EQ(report.at("validations").as_uint64(), 1);
|
||||
EXPECT_EQ(report.at("manifests").as_uint64(), 1);
|
||||
EXPECT_EQ(report.at("book_changes").as_uint64(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, StreamsLedger)
|
||||
{
|
||||
static auto constexpr expectedOutput =
|
||||
R"({
|
||||
"validated_ledgers":"10-30",
|
||||
"ledger_index":30,
|
||||
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
|
||||
"ledger_time":0,
|
||||
"fee_ref":4,
|
||||
"fee_base":1,
|
||||
"reserve_base":3,
|
||||
"reserve_inc":2
|
||||
})";
|
||||
mockBackendPtr->updateRange(MINSEQ);
|
||||
mockBackendPtr->updateRange(MAXSEQ);
|
||||
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
|
||||
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
|
||||
// return valid ledgerinfo
|
||||
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ);
|
||||
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ, _)).WillByDefault(Return(ledgerinfo));
|
||||
// fee
|
||||
auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(feeBlob));
|
||||
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
|
||||
|
||||
// ledger stream returns information about the ledgers on hand and current
|
||||
// fee schedule.
|
||||
auto const input = json::parse(
|
||||
R"({
|
||||
"streams": ["ledger"]
|
||||
})");
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output->as_object(), json::parse(expectedOutput));
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
EXPECT_EQ(report.at("ledger").as_uint64(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, Accounts)
|
||||
{
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"accounts": ["{}","{}","{}"]
|
||||
}})",
|
||||
ACCOUNT,
|
||||
ACCOUNT2,
|
||||
ACCOUNT2));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
// filter the duplicates
|
||||
EXPECT_EQ(report.at("account").as_uint64(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, AccountsProposed)
|
||||
{
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"accounts_proposed": ["{}","{}","{}"]
|
||||
}})",
|
||||
ACCOUNT,
|
||||
ACCOUNT2,
|
||||
ACCOUNT2));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
// filter the duplicates
|
||||
EXPECT_EQ(report.at("accounts_proposed").as_uint64(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, JustBooks)
|
||||
{
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"books":
|
||||
[
|
||||
{{
|
||||
"taker_pays":
|
||||
{{
|
||||
"currency": "XRP"
|
||||
}},
|
||||
"taker_gets":
|
||||
{{
|
||||
"currency": "USD",
|
||||
"issuer": "{}"
|
||||
}}
|
||||
}}
|
||||
]
|
||||
}})",
|
||||
ACCOUNT));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
EXPECT_EQ(report.at("books").as_uint64(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, BooksBothSet)
|
||||
{
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"books":
|
||||
[
|
||||
{{
|
||||
"taker_pays":
|
||||
{{
|
||||
"currency": "XRP"
|
||||
}},
|
||||
"taker_gets":
|
||||
{{
|
||||
"currency": "USD",
|
||||
"issuer": "{}"
|
||||
}},
|
||||
"both": true
|
||||
}}
|
||||
]
|
||||
}})",
|
||||
ACCOUNT));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output->as_object().empty());
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
// original book + reverse book
|
||||
EXPECT_EQ(report.at("books").as_uint64(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCSubscribeHandlerTest, BooksBothSnapshotSet)
|
||||
{
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"books":
|
||||
[
|
||||
{{
|
||||
"taker_gets":
|
||||
{{
|
||||
"currency": "XRP"
|
||||
}},
|
||||
"taker_pays":
|
||||
{{
|
||||
"currency": "USD",
|
||||
"issuer": "{}"
|
||||
}},
|
||||
"both": true,
|
||||
"snapshot": true
|
||||
}}
|
||||
]
|
||||
}})",
|
||||
ACCOUNT));
|
||||
mockBackendPtr->updateRange(MINSEQ);
|
||||
mockBackendPtr->updateRange(MAXSEQ);
|
||||
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
|
||||
auto const issuer = GetAccountIDWithString(ACCOUNT);
|
||||
|
||||
auto const getsXRPPaysUSDBook = getBookBase(std::get<ripple::Book>(RPC::parseBook(
|
||||
ripple::to_currency("USD"), // pays
|
||||
issuer,
|
||||
ripple::xrpCurrency(), // gets
|
||||
ripple::xrpAccount())));
|
||||
|
||||
auto const reversedBook = getBookBase(std::get<ripple::Book>(RPC::parseBook(
|
||||
ripple::xrpCurrency(), // pays
|
||||
ripple::xrpAccount(),
|
||||
ripple::to_currency("USD"), // gets
|
||||
issuer)));
|
||||
|
||||
ON_CALL(*rawBackendPtr, doFetchSuccessorKey(getsXRPPaysUSDBook, MAXSEQ, _))
|
||||
.WillByDefault(Return(ripple::uint256{PAYS20USDGETS10XRPBOOKDIR}));
|
||||
|
||||
ON_CALL(*rawBackendPtr, doFetchSuccessorKey(ripple::uint256{PAYS20USDGETS10XRPBOOKDIR}, MAXSEQ, _))
|
||||
.WillByDefault(Return(std::nullopt));
|
||||
|
||||
ON_CALL(*rawBackendPtr, doFetchSuccessorKey(reversedBook, MAXSEQ, _))
|
||||
.WillByDefault(Return(ripple::uint256{PAYS20XRPGETS10USDBOOKDIR}));
|
||||
|
||||
EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(4);
|
||||
|
||||
// 2 book dirs + 2 issuer global freeze + 2 transferRate + 1 owner root + 1 fee
|
||||
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(8);
|
||||
|
||||
auto const indexes = std::vector<ripple::uint256>(10, ripple::uint256{INDEX2});
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::uint256{PAYS20USDGETS10XRPBOOKDIR}, MAXSEQ, _))
|
||||
.WillByDefault(Return(CreateOwnerDirLedgerObject(indexes, INDEX1).getSerializer().peekData()));
|
||||
|
||||
// for reverse
|
||||
auto const indexes2 = std::vector<ripple::uint256>(10, ripple::uint256{INDEX1});
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::uint256{PAYS20XRPGETS10USDBOOKDIR}, MAXSEQ, _))
|
||||
.WillByDefault(Return(CreateOwnerDirLedgerObject(indexes2, INDEX2).getSerializer().peekData()));
|
||||
|
||||
// offer owner account root
|
||||
ON_CALL(
|
||||
*rawBackendPtr, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, MAXSEQ, _))
|
||||
.WillByDefault(Return(CreateAccountRootObject(ACCOUNT2, 0, 2, 200, 2, INDEX1, 2).getSerializer().peekData()));
|
||||
|
||||
// issuer account root
|
||||
ON_CALL(
|
||||
*rawBackendPtr, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, MAXSEQ, _))
|
||||
.WillByDefault(Return(CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2).getSerializer().peekData()));
|
||||
|
||||
// fee
|
||||
auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::keylet::fees().key, MAXSEQ, _)).WillByDefault(Return(feeBlob));
|
||||
|
||||
auto const gets10XRPPays20USDOffer = CreateOfferLedgerObject(
|
||||
ACCOUNT2,
|
||||
10,
|
||||
20,
|
||||
ripple::to_string(ripple::xrpCurrency()),
|
||||
ripple::to_string(ripple::to_currency("USD")),
|
||||
toBase58(ripple::xrpAccount()),
|
||||
ACCOUNT,
|
||||
PAYS20USDGETS10XRPBOOKDIR);
|
||||
|
||||
// for reverse
|
||||
// offer owner is USD issuer
|
||||
auto const gets10USDPays20XRPOffer = CreateOfferLedgerObject(
|
||||
ACCOUNT,
|
||||
10,
|
||||
20,
|
||||
ripple::to_string(ripple::to_currency("USD")),
|
||||
ripple::to_string(ripple::xrpCurrency()),
|
||||
ACCOUNT,
|
||||
toBase58(ripple::xrpAccount()),
|
||||
PAYS20XRPGETS10USDBOOKDIR);
|
||||
|
||||
std::vector<Blob> bbs(10, gets10XRPPays20USDOffer.getSerializer().peekData());
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObjects(indexes, MAXSEQ, _)).WillByDefault(Return(bbs));
|
||||
|
||||
// for reverse
|
||||
std::vector<Blob> bbs2(10, gets10USDPays20XRPOffer.getSerializer().peekData());
|
||||
ON_CALL(*rawBackendPtr, doFetchLedgerObjects(indexes2, MAXSEQ, _)).WillByDefault(Return(bbs2));
|
||||
|
||||
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(2);
|
||||
|
||||
static auto const expectedOffer = fmt::format(
|
||||
R"({{
|
||||
"Account":"{}",
|
||||
"BookDirectory":"{}",
|
||||
"BookNode":"0",
|
||||
"Flags":0,
|
||||
"LedgerEntryType":"Offer",
|
||||
"OwnerNode":"0",
|
||||
"PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"PreviousTxnLgrSeq":0,
|
||||
"Sequence":0,
|
||||
"TakerGets":"10",
|
||||
"TakerPays":
|
||||
{{
|
||||
"currency":"USD",
|
||||
"issuer":"{}",
|
||||
"value":"20"
|
||||
}},
|
||||
"index":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
|
||||
"owner_funds":"193",
|
||||
"quality":"2"
|
||||
}})",
|
||||
ACCOUNT2,
|
||||
PAYS20USDGETS10XRPBOOKDIR,
|
||||
ACCOUNT);
|
||||
static auto const expectedReversedOffer = fmt::format(
|
||||
R"({{
|
||||
"Account":"{}",
|
||||
"BookDirectory":"{}",
|
||||
"BookNode":"0",
|
||||
"Flags":0,
|
||||
"LedgerEntryType":"Offer",
|
||||
"OwnerNode":"0",
|
||||
"PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"PreviousTxnLgrSeq":0,
|
||||
"Sequence":0,
|
||||
"TakerGets":
|
||||
{{
|
||||
"currency":"USD",
|
||||
"issuer":"{}",
|
||||
"value":"10"
|
||||
}},
|
||||
"TakerPays":"20",
|
||||
"index":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
|
||||
"owner_funds":"10",
|
||||
"quality":"2"
|
||||
}})",
|
||||
ACCOUNT,
|
||||
PAYS20XRPGETS10USDBOOKDIR,
|
||||
ACCOUNT);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{mockBackendPtr, subManager_}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield), session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output->as_object().at("offers").as_array().size(), 20);
|
||||
EXPECT_EQ(output->as_object().at("offers").as_array()[0].as_object(), json::parse(expectedOffer));
|
||||
EXPECT_EQ(output->as_object().at("offers").as_array()[10].as_object(), json::parse(expectedReversedOffer));
|
||||
std::this_thread::sleep_for(20ms);
|
||||
auto const report = subManager_->report();
|
||||
// original book + reverse book
|
||||
EXPECT_EQ(report.at("books").as_uint64(), 2);
|
||||
});
|
||||
}
|
||||
@@ -285,6 +285,7 @@ protected:
|
||||
*/
|
||||
struct HandlerBaseTest : public MockBackendTest, public SyncAsioContextTest
|
||||
{
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user