Subscribe handler (#591)

Fixes #593
This commit is contained in:
cyan317
2023-04-13 14:14:11 +01:00
committed by GitHub
parent 36bb20806e
commit 0bc84fefbf
17 changed files with 1365 additions and 46 deletions

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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_ = {};
};

View File

@@ -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);
}

View File

@@ -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");
});
}

View File

@@ -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})",

View File

@@ -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");
});
}

View File

@@ -80,7 +80,7 @@ generateTestValuesForParametersTest()
{
"AccountInvalid",
R"({"account": "123"})",
"invalidParams",
"actMalformed",
"accountMalformed",
},
{

View File

@@ -255,7 +255,7 @@ generateParameterBookOffersTestBundles()
},
"taker": "123"
})",
"invalidParams",
"actMalformed",
"takerMalformed"},
ParameterTestBundle{
"TakerNotString",

View File

@@ -98,7 +98,7 @@ generateParameterTestBundles()
R"({
"account": "1213"
})",
"invalidParams",
"actMalformed",
"accountMalformed"},
ParameterTestBundle{
"LedgerIndexInvalid",

View File

@@ -88,7 +88,7 @@ generateTestValuesForParametersTest()
"account": "123",
"role": "gateway"
})",
"invalidParams",
"actMalformed",
"accountMalformed"},
NoRippleParamTestCaseBundle{
"InvalidRole",

View 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);
});
}

View File

@@ -285,6 +285,7 @@ protected:
*/
struct HandlerBaseTest : public MockBackendTest, public SyncAsioContextTest
{
protected:
void
SetUp() override
{