Compare commits

...

10 Commits

Author SHA1 Message Date
cyan317
4f3b6e98ad Subscribe cleanup (#940)
Fix #939
2023-12-19 15:52:12 +00:00
cyan317
27279ceb6d Fix messages pile up (#921)
Fix #924
2023-12-19 15:51:39 +00:00
Alex Kremer
7a1f902f42 Fix http params handling discrepancy (#913)
Fixes #909
2023-10-11 00:40:41 +01:00
Alex Kremer
7e621b2518 Add field name to output of invalidParams for OneOf (#906)
Fixes #901
2023-10-11 00:39:01 +01:00
cyan317
e32e2ebee4 Fix account_tx response both both ledger range and ledger index/hash are specified (#904)
Fix mismatch with rippled
2023-10-11 00:34:01 +01:00
Alex Kremer
7742c4a5e3 Add inLedger to tx and account_tx (#895)
Fixes #890
2023-10-11 00:32:58 +01:00
cyan317
c24c3b536f Fix trans order of subscription transactions stream (#882)
Fix #833
2023-10-11 00:26:06 +01:00
Alex Kremer
4d42f7c4e4 Change consume to full buffer recreate (#899) 2023-10-11 00:10:20 +01:00
cyan317
c634f0f0ba Fix ledger_entry error code (#891)
Fix #896
2023-10-10 23:03:12 +01:00
Alex Kremer
2ef766a740 Fixes broken counters for broken pipe connections (#880)
Fixes #885
2023-10-10 23:03:04 +01:00
30 changed files with 1036 additions and 348 deletions

View File

@@ -19,13 +19,7 @@ set(COMPILER_FLAGS
-Wunused -Wunused
) )
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if (is_gcc AND NOT lint)
list(APPEND COMPILER_FLAGS
-Wshadow # gcc is to aggressive with shadowing https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78147
)
endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND COMPILER_FLAGS list(APPEND COMPILER_FLAGS
-Wduplicated-branches -Wduplicated-branches
-Wduplicated-cond -Wduplicated-cond
@@ -34,6 +28,18 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
) )
endif () endif ()
if (is_clang)
list(APPEND COMPILER_FLAGS
-Wshadow # gcc is to aggressive with shadowing https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78147
)
endif ()
if (is_appleclang)
list(APPEND COMPILER_FLAGS
-Wreorder-init-list
)
endif ()
# See https://github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#gcc--clang for the flags description # See https://github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#gcc--clang for the flags description
target_compile_options (clio PUBLIC ${COMPILER_FLAGS}) target_compile_options (clio PUBLIC ${COMPILER_FLAGS})

View File

@@ -171,6 +171,16 @@ public:
subscriptions_->pubLedger(lgrInfo, *fees, range, transactions.size()); subscriptions_->pubLedger(lgrInfo, *fees, range, transactions.size());
// order with transaction index
std::sort(transactions.begin(), transactions.end(), [](auto const& t1, auto const& t2) {
ripple::SerialIter iter1{t1.metadata.data(), t1.metadata.size()};
ripple::STObject const object1(iter1, ripple::sfMetadata);
ripple::SerialIter iter2{t2.metadata.data(), t2.metadata.size()};
ripple::STObject const object2(iter2, ripple::sfMetadata);
return object1.getFieldU32(ripple::sfTransactionIndex) <
object2.getFieldU32(ripple::sfTransactionIndex);
});
for (auto& txAndMeta : transactions) for (auto& txAndMeta : transactions)
subscriptions_->pubTransaction(txAndMeta, lgrInfo); subscriptions_->pubTransaction(txAndMeta, lgrInfo);

View File

@@ -35,6 +35,12 @@ Subscription::unsubscribe(SessionPtrType const& session)
boost::asio::post(strand_, [this, session]() { removeSession(session, subscribers_, subCount_); }); boost::asio::post(strand_, [this, session]() { removeSession(session, subscribers_, subCount_); });
} }
bool
Subscription::hasSession(SessionPtrType const& session)
{
return subscribers_.contains(session);
}
void void
Subscription::publish(std::shared_ptr<std::string> const& message) Subscription::publish(std::shared_ptr<std::string> const& message)
{ {
@@ -334,6 +340,8 @@ SubscriptionManager::unsubProposedTransactions(SessionPtrType session)
void void
SubscriptionManager::subscribeHelper(SessionPtrType const& session, Subscription& subs, CleanupFunction&& func) SubscriptionManager::subscribeHelper(SessionPtrType const& session, Subscription& subs, CleanupFunction&& func)
{ {
if (subs.hasSession(session))
return;
subs.subscribe(session); subs.subscribe(session);
std::scoped_lock lk(cleanupMtx_); std::scoped_lock lk(cleanupMtx_);
cleanupFuncs_[session].push_back(std::move(func)); cleanupFuncs_[session].push_back(std::move(func));
@@ -347,6 +355,8 @@ SubscriptionManager::subscribeHelper(
SubscriptionMap<Key>& subs, SubscriptionMap<Key>& subs,
CleanupFunction&& func) CleanupFunction&& func)
{ {
if (subs.hasSession(session, k))
return;
subs.subscribe(session, k); subs.subscribe(session, k);
std::scoped_lock lk(cleanupMtx_); std::scoped_lock lk(cleanupMtx_);
cleanupFuncs_[session].push_back(std::move(func)); cleanupFuncs_[session].push_back(std::move(func));

View File

@@ -139,6 +139,15 @@ public:
void void
unsubscribe(SessionPtrType const& session); unsubscribe(SessionPtrType const& session);
/**
* @brief Check if a session has been in subscribers list.
*
* @param session The session to check
* @return true if the session is in the subscribers list; false otherwise
*/
bool
hasSession(SessionPtrType const& session);
/** /**
* @brief Sends the given message to all subscribers. * @brief Sends the given message to all subscribers.
* *
@@ -232,6 +241,22 @@ public:
}); });
} }
/**
* @brief Check if a session has been in subscribers list.
*
* @param session The session to check
* @param key The key for the subscription to check
* @return true if the session is in the subscribers list; false otherwise
*/
bool
hasSession(SessionPtrType const& session, Key const& key)
{
if (!subscribers_.contains(key))
return false;
return subscribers_[key].contains(session);
}
/** /**
* @brief Sends the given message to all subscribers. * @brief Sends the given message to all subscribers.
* *

View File

@@ -150,11 +150,9 @@ public:
if (v) if (v)
return v->as_object(); return v->as_object();
else
{ notifyErrored(ctx.method);
notifyErrored(ctx.method); return Status{v.error()};
return Status{v.error()};
}
} }
catch (data::DatabaseTimeout const& t) catch (data::DatabaseTimeout const& t)
{ {

View File

@@ -1149,21 +1149,15 @@ parseBook(ripple::Currency pays, ripple::AccountID payIssuer, ripple::Currency g
{ {
if (isXRP(pays) && !isXRP(payIssuer)) if (isXRP(pays) && !isXRP(payIssuer))
return Status{ return Status{
RippledError::rpcSRC_ISR_MALFORMED, RippledError::rpcSRC_ISR_MALFORMED, "Unneeded field 'taker_pays.issuer' for XRP currency specification."};
"Unneeded field 'taker_pays.issuer' for XRP currency "
"specification."};
if (!isXRP(pays) && isXRP(payIssuer)) if (!isXRP(pays) && isXRP(payIssuer))
return Status{ return Status{
RippledError::rpcSRC_ISR_MALFORMED, RippledError::rpcSRC_ISR_MALFORMED, "Invalid field 'taker_pays.issuer', expected non-XRP issuer."};
"Invalid field 'taker_pays.issuer', expected non-XRP "
"issuer."};
if (ripple::isXRP(gets) && !ripple::isXRP(getIssuer)) if (ripple::isXRP(gets) && !ripple::isXRP(getIssuer))
return Status{ return Status{
RippledError::rpcDST_ISR_MALFORMED, RippledError::rpcDST_ISR_MALFORMED, "Unneeded field 'taker_gets.issuer' for XRP currency specification."};
"Unneeded field 'taker_gets.issuer' for XRP currency "
"specification."};
if (!ripple::isXRP(gets) && ripple::isXRP(getIssuer)) if (!ripple::isXRP(gets) && ripple::isXRP(getIssuer))
return Status{ return Status{
@@ -1233,15 +1227,11 @@ parseBook(boost::json::object const& request)
if (isXRP(pay_currency) && !isXRP(pay_issuer)) if (isXRP(pay_currency) && !isXRP(pay_issuer))
return Status{ return Status{
RippledError::rpcSRC_ISR_MALFORMED, RippledError::rpcSRC_ISR_MALFORMED, "Unneeded field 'taker_pays.issuer' for XRP currency specification."};
"Unneeded field 'taker_pays.issuer' for XRP currency "
"specification."};
if (!isXRP(pay_currency) && isXRP(pay_issuer)) if (!isXRP(pay_currency) && isXRP(pay_issuer))
return Status{ return Status{
RippledError::rpcSRC_ISR_MALFORMED, RippledError::rpcSRC_ISR_MALFORMED, "Invalid field 'taker_pays.issuer', expected non-XRP issuer."};
"Invalid field 'taker_pays.issuer', expected non-XRP "
"issuer."};
if ((!isXRP(pay_currency)) && (!taker_pays.contains("issuer"))) if ((!isXRP(pay_currency)) && (!taker_pays.contains("issuer")))
return Status{RippledError::rpcSRC_ISR_MALFORMED, "Missing non-XRP issuer."}; return Status{RippledError::rpcSRC_ISR_MALFORMED, "Missing non-XRP issuer."};
@@ -1258,9 +1248,7 @@ parseBook(boost::json::object const& request)
if (get_issuer == ripple::noAccount()) if (get_issuer == ripple::noAccount())
return Status{ return Status{
RippledError::rpcDST_ISR_MALFORMED, RippledError::rpcDST_ISR_MALFORMED, "Invalid field 'taker_gets.issuer', bad issuer account one."};
"Invalid field 'taker_gets.issuer', bad issuer account "
"one."};
} }
else else
{ {
@@ -1269,9 +1257,7 @@ parseBook(boost::json::object const& request)
if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer)) if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer))
return Status{ return Status{
RippledError::rpcDST_ISR_MALFORMED, RippledError::rpcDST_ISR_MALFORMED, "Unneeded field 'taker_gets.issuer' for XRP currency specification."};
"Unneeded field 'taker_gets.issuer' for XRP currency "
"specification."};
if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer)) if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer))
return Status{ return Status{

View File

@@ -146,11 +146,7 @@ CustomValidator IssuerValidator =
if (issuer == ripple::noAccount()) if (issuer == ripple::noAccount())
return Error{Status{ return Error{Status{
RippledError::rpcINVALID_PARAMS, RippledError::rpcINVALID_PARAMS, fmt::format("Invalid field '{}', bad issuer account one.", key)}};
fmt::format(
"Invalid field '{}', bad issuer account "
"one.",
key)}};
return MaybeError{}; return MaybeError{};
}}; }};

View File

@@ -417,7 +417,7 @@ public:
auto const res = value_to<Type>(value.as_object().at(key.data())); auto const res = value_to<Type>(value.as_object().at(key.data()));
if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_)) if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_))
return Error{Status{RippledError::rpcINVALID_PARAMS}}; return Error{Status{RippledError::rpcINVALID_PARAMS, fmt::format("Invalid field '{}'.", key)}};
return {}; return {};
} }

View File

@@ -107,13 +107,18 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) con
if (ctx.apiVersion > 1u && (input.ledgerIndexMax || input.ledgerIndexMin)) if (ctx.apiVersion > 1u && (input.ledgerIndexMax || input.ledgerIndexMin))
return Error{Status{RippledError::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"}}; return Error{Status{RippledError::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"}};
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq( if (!input.ledgerIndexMax && !input.ledgerIndexMin)
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence); {
// mimic rippled, when both range and index specified, respect the range.
// take ledger from ledgerHash or ledgerIndex only when range is not specified
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq(
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence);
if (auto status = std::get_if<Status>(&lgrInfoOrStatus)) if (auto status = std::get_if<Status>(&lgrInfoOrStatus))
return Error{*status}; return Error{*status};
maxIndex = minIndex = std::get<ripple::LedgerHeader>(lgrInfoOrStatus).seq; maxIndex = minIndex = std::get<ripple::LedgerHeader>(lgrInfoOrStatus).seq;
}
} }
std::optional<data::TransactionsCursor> cursor; std::optional<data::TransactionsCursor> cursor;
@@ -180,8 +185,11 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) con
continue; continue;
} }
obj[JS(tx)].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date; obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date;
obj[JS(tx)].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
if (ctx.apiVersion < 2u)
obj[JS(tx)].as_object()[JS(inLedger)] = txnPlusMeta.ledgerSequence;
} }
else else
{ {

View File

@@ -95,7 +95,7 @@ public:
// return INVALID_PARAMS if account format is wrong for "taker" // return INVALID_PARAMS if account format is wrong for "taker"
{JS(taker), {JS(taker),
meta::WithCustomError{ meta::WithCustomError{
validation::AccountValidator, Status(RippledError::rpcINVALID_PARAMS, "Invalid field 'taker'")}}, validation::AccountValidator, Status(RippledError::rpcINVALID_PARAMS, "Invalid field 'taker'.")}},
{JS(limit), {JS(limit),
validation::Type<uint32_t>{}, validation::Type<uint32_t>{},
validation::Min(1u), validation::Min(1u),

View File

@@ -100,9 +100,7 @@ public:
meta::WithCustomError{ meta::WithCustomError{
validation::Type<std::string>{}, validation::Type<std::string>{},
Status{ripple::rpcINVALID_PARAMS, "Invalid field 'type', not string."}}, Status{ripple::rpcINVALID_PARAMS, "Invalid field 'type', not string."}},
meta::WithCustomError{ validation::OneOf<std::string>(TYPES_KEYS.cbegin(), TYPES_KEYS.cend())},
validation::OneOf<std::string>(TYPES_KEYS.cbegin(), TYPES_KEYS.cend()),
Status{ripple::rpcINVALID_PARAMS, "Invalid field 'type'."}}},
}; };
return rpcSpec; return rpcSpec;

View File

@@ -83,8 +83,8 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
{ {
// Must specify 1 of the following fields to indicate what type // Must specify 1 of the following fields to indicate what type
if (ctx.apiVersion == 1) if (ctx.apiVersion == 1)
return Error{Status{RippledError::rpcINVALID_PARAMS}}; return Error{Status{ClioError::rpcUNKNOWN_OPTION}};
return Error{Status{ClioError::rpcUNKNOWN_OPTION}}; return Error{Status{RippledError::rpcINVALID_PARAMS}};
} }
// check ledger exists // check ledger exists

View File

@@ -36,7 +36,7 @@ TxHandler::process(Input input, Context const& ctx) const
return Error{Status{RippledError::rpcEXCESSIVE_LGR_RANGE}}; return Error{Status{RippledError::rpcEXCESSIVE_LGR_RANGE}};
} }
auto output = TxHandler::Output{}; auto output = TxHandler::Output{.apiVersion = ctx.apiVersion};
auto const dbResponse = auto const dbResponse =
sharedPtrBackend_->fetchTransaction(ripple::uint256{std::string_view(input.transaction)}, ctx.yield); sharedPtrBackend_->fetchTransaction(ripple::uint256{std::string_view(input.transaction)}, ctx.yield);
@@ -55,7 +55,6 @@ TxHandler::process(Input input, Context const& ctx) const
return Error{Status{RippledError::rpcTXN_NOT_FOUND}}; return Error{Status{RippledError::rpcTXN_NOT_FOUND}};
} }
// clio does not implement 'inLedger' which is a deprecated field
if (!input.binary) if (!input.binary)
{ {
auto const [txn, meta] = toExpandedJson(*dbResponse, NFTokenjson::ENABLE); auto const [txn, meta] = toExpandedJson(*dbResponse, NFTokenjson::ENABLE);
@@ -95,6 +94,9 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TxHandler::Outpu
obj[JS(date)] = output.date; obj[JS(date)] = output.date;
obj[JS(ledger_index)] = output.ledgerIndex; obj[JS(ledger_index)] = output.ledgerIndex;
if (output.apiVersion < 2u)
obj[JS(inLedger)] = output.ledgerIndex;
jv = std::move(obj); jv = std::move(obj);
} }

View File

@@ -38,13 +38,14 @@ class TxHandler
public: public:
struct Output struct Output
{ {
uint32_t date; uint32_t date = 0u;
std::string hash; std::string hash{};
uint32_t ledgerIndex; uint32_t ledgerIndex = 0u;
std::optional<boost::json::object> meta; std::optional<boost::json::object> meta{};
std::optional<boost::json::object> tx; std::optional<boost::json::object> tx{};
std::optional<std::string> metaStr; std::optional<std::string> metaStr{};
std::optional<std::string> txStr; std::optional<std::string> txStr{};
uint32_t apiVersion = 0u;
bool validated = true; bool validated = true;
}; };

View File

@@ -89,8 +89,8 @@ public:
auto req = boost::json::parse(request).as_object(); auto req = boost::json::parse(request).as_object();
LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue"; LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
if (not connection->upgraded and not req.contains("params")) if (not connection->upgraded and shouldReplaceParams(req))
req["params"] = boost::json::array({boost::json::object{}}); req[JS(params)] = boost::json::array({boost::json::object{}});
if (!rpcEngine_->post( if (!rpcEngine_->post(
[this, request = std::move(req), connection](boost::asio::yield_context yield) mutable { [this, request = std::move(req), connection](boost::asio::yield_context yield) mutable {
@@ -267,6 +267,27 @@ private:
return web::detail::ErrorHelper(connection, request).sendInternalError(); return web::detail::ErrorHelper(connection, request).sendInternalError();
} }
} }
bool
shouldReplaceParams(boost::json::object const& req) const
{
auto const hasParams = req.contains(JS(params));
auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
auto const paramsIsEmptyString =
hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
auto const paramsIsEmptyObject =
hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and
req.at(JS(params)).as_array().at(0).as_string().empty();
// Note: all this compatibility dance is to match `rippled` as close as possible
return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or
firstArgIsEmptyString or firstArgIsNull;
}
}; };
} // namespace web } // namespace web

View File

@@ -180,10 +180,10 @@ public:
if (boost::beast::websocket::is_upgrade(req_)) if (boost::beast::websocket::is_upgrade(req_))
{ {
upgraded = true; // Disable the timeout. The websocket::stream uses its own timeout settings.
// Disable the timeout.
// The websocket::stream uses its own timeout settings.
boost::beast::get_lowest_layer(derived().stream()).expires_never(); boost::beast::get_lowest_layer(derived().stream()).expires_never();
upgraded = true;
return derived().upgrade(); return derived().upgrade();
} }

View File

@@ -63,7 +63,7 @@ protected:
if (!ec_ && ec != boost::asio::error::operation_aborted) if (!ec_ && ec != boost::asio::error::operation_aborted)
{ {
ec_ = ec; ec_ = ec;
LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message(); LOG(perfLog_.error()) << tag() << ": " << what << ": " << ec.message();
boost::beast::get_lowest_layer(derived().ws()).socket().close(ec); boost::beast::get_lowest_layer(derived().ws()).socket().close(ec);
(*handler_)(ec, derived().shared_from_this()); (*handler_)(ec, derived().shared_from_this());
} }
@@ -106,14 +106,14 @@ public:
void void
onWrite(boost::system::error_code ec, std::size_t) onWrite(boost::system::error_code ec, std::size_t)
{ {
messages_.pop();
sending_ = false;
if (ec) if (ec)
{ {
wsFail(ec, "Failed to write"); wsFail(ec, "Failed to write");
} }
else else
{ {
messages_.pop();
sending_ = false;
maybeSendNext(); maybeSendNext();
} }
} }
@@ -121,6 +121,10 @@ public:
void void
maybeSendNext() maybeSendNext()
{ {
// cleanup if needed. can't do this in destructor so it's here
if (dead())
(*handler_)(ec_, derived().shared_from_this());
if (ec_ || sending_ || messages_.empty()) if (ec_ || sending_ || messages_.empty())
return; return;
@@ -204,8 +208,8 @@ public:
if (dead()) if (dead())
return; return;
// Clear the buffer // Note: use entirely new buffer so previously used, potentially large, capacity is deallocated
buffer_.consume(buffer_.size()); buffer_ = boost::beast::flat_buffer{};
derived().ws().async_read(buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this())); derived().ws().async_read(buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this()));
} }

View File

@@ -161,7 +161,6 @@ TEST_F(SubscriptionManagerSimpleBackendTest, ReportCurrentSubscriber)
EXPECT_EQ(reportReturn["books"], result); EXPECT_EQ(reportReturn["books"], result);
}; };
checkResult(subManagerPtr->report(), 1); checkResult(subManagerPtr->report(), 1);
subManagerPtr->cleanup(session2);
subManagerPtr->cleanup(session2); // clean a removed session subManagerPtr->cleanup(session2); // clean a removed session
std::this_thread::sleep_for(20ms); std::this_thread::sleep_for(20ms);
checkResult(subManagerPtr->report(), 0); checkResult(subManagerPtr->report(), 0);

View File

@@ -53,6 +53,8 @@ TEST_F(SubscriptionTest, SubscriptionCount)
ctx.restart(); ctx.restart();
ctx.run(); ctx.run();
EXPECT_EQ(sub.count(), 2); EXPECT_EQ(sub.count(), 2);
EXPECT_TRUE(sub.hasSession(session1));
EXPECT_TRUE(sub.hasSession(session2));
EXPECT_FALSE(sub.empty()); EXPECT_FALSE(sub.empty());
sub.unsubscribe(session1); sub.unsubscribe(session1);
ctx.restart(); ctx.restart();
@@ -67,6 +69,8 @@ TEST_F(SubscriptionTest, SubscriptionCount)
ctx.run(); ctx.run();
EXPECT_EQ(sub.count(), 0); EXPECT_EQ(sub.count(), 0);
EXPECT_TRUE(sub.empty()); EXPECT_TRUE(sub.empty());
EXPECT_FALSE(sub.hasSession(session1));
EXPECT_FALSE(sub.hasSession(session2));
} }
// send interface will be called when publish called // send interface will be called when publish called
@@ -131,6 +135,9 @@ TEST_F(SubscriptionMapTest, SubscriptionMapCount)
ctx.restart(); ctx.restart();
ctx.run(); ctx.run();
EXPECT_EQ(subMap.count(), 3); EXPECT_EQ(subMap.count(), 3);
EXPECT_TRUE(subMap.hasSession(session1, "topic1"));
EXPECT_TRUE(subMap.hasSession(session2, "topic1"));
EXPECT_TRUE(subMap.hasSession(session3, "topic2"));
subMap.unsubscribe(session1, "topic1"); subMap.unsubscribe(session1, "topic1");
ctx.restart(); ctx.restart();
ctx.run(); ctx.run();
@@ -139,6 +146,9 @@ TEST_F(SubscriptionMapTest, SubscriptionMapCount)
subMap.unsubscribe(session3, "topic2"); subMap.unsubscribe(session3, "topic2");
ctx.restart(); ctx.restart();
ctx.run(); ctx.run();
EXPECT_FALSE(subMap.hasSession(session1, "topic1"));
EXPECT_FALSE(subMap.hasSession(session2, "topic1"));
EXPECT_FALSE(subMap.hasSession(session3, "topic2"));
EXPECT_EQ(subMap.count(), 0); EXPECT_EQ(subMap.count(), 0);
subMap.unsubscribe(session3, "topic2"); subMap.unsubscribe(session3, "topic2");
subMap.unsubscribe(session3, "no exist"); subMap.unsubscribe(session3, "no exist");

View File

@@ -0,0 +1,304 @@
//------------------------------------------------------------------------------
/*
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 <etl/impl/LedgerPublisher.h>
#include <util/Fixtures.h>
#include <util/MockCache.h>
#include <util/TestObject.h>
#include <fmt/core.h>
#include <gtest/gtest.h>
#include <chrono>
using namespace testing;
using namespace etl;
namespace json = boost::json;
using namespace std::chrono;
static auto constexpr ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
static auto constexpr ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
static auto constexpr LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
static auto constexpr SEQ = 30;
static auto constexpr AGE = 800;
class ETLLedgerPublisherTest : public MockBackendTest, public SyncAsioContextTest, public MockSubscriptionManagerTest
{
void
SetUp() override
{
MockBackendTest::SetUp();
SyncAsioContextTest::SetUp();
MockSubscriptionManagerTest::SetUp();
}
void
TearDown() override
{
MockSubscriptionManagerTest::TearDown();
SyncAsioContextTest::TearDown();
MockBackendTest::TearDown();
}
protected:
util::Config cfg{json::parse("{}")};
MockCache mockCache;
};
TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoIsWritingFalse)
{
SystemState dummyState;
dummyState.isWriting = false;
auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, AGE);
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
publisher.publish(dummyLedgerInfo);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
ON_CALL(*rawBackendPtr, fetchLedgerDiff(SEQ, _)).WillByDefault(Return(std::vector<LedgerObject>{}));
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(SEQ, _)).Times(1);
// setLastPublishedSequence not in strand, should verify before run
EXPECT_TRUE(publisher.getLastPublishedSequence());
EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
EXPECT_CALL(mockCache, updateImp).Times(1);
ctx.run();
EXPECT_TRUE(rawBackendPtr->fetchLedgerRange());
EXPECT_EQ(rawBackendPtr->fetchLedgerRange().value().minSequence, SEQ);
EXPECT_EQ(rawBackendPtr->fetchLedgerRange().value().maxSequence, SEQ);
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoIsWritingTrue)
{
SystemState dummyState;
dummyState.isWriting = true;
auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, AGE);
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
publisher.publish(dummyLedgerInfo);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(_, _)).Times(0);
// setLastPublishedSequence not in strand, should verify before run
EXPECT_TRUE(publisher.getLastPublishedSequence());
EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
ctx.run();
EXPECT_FALSE(rawBackendPtr->fetchLedgerRange());
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoInRange)
{
SystemState dummyState;
dummyState.isWriting = true;
auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, 0); // age is 0
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
mockBackendPtr->updateRange(SEQ - 1);
mockBackendPtr->updateRange(SEQ);
publisher.publish(dummyLedgerInfo);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(_, _)).Times(0);
// mock fetch fee
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
.WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0)));
// mock fetch transactions
EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger).Times(1);
TransactionAndMetadata t1;
t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData();
t1.ledgerSequence = SEQ;
ON_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(SEQ, _))
.WillByDefault(Return(std::vector<TransactionAndMetadata>{t1}));
// setLastPublishedSequence not in strand, should verify before run
EXPECT_TRUE(publisher.getLastPublishedSequence());
EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
MockSubscriptionManager* rawSubscriptionManagerPtr =
dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1)).Times(1);
EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
// mock 1 transaction
EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction).Times(1);
ctx.run();
// last publish time should be set
EXPECT_TRUE(publisher.lastPublishAgeSeconds() <= 1);
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerInfoCloseTimeGreaterThanNow)
{
SystemState dummyState;
dummyState.isWriting = true;
ripple::LedgerInfo dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, 0);
auto const nowPlus10 = system_clock::now() + seconds(10);
auto const closeTime = duration_cast<seconds>(nowPlus10.time_since_epoch()).count() - rippleEpochStart;
dummyLedgerInfo.closeTime = ripple::NetClock::time_point{seconds{closeTime}};
mockBackendPtr->updateRange(SEQ - 1);
mockBackendPtr->updateRange(SEQ);
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
publisher.publish(dummyLedgerInfo);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(_, _)).Times(0);
// mock fetch fee
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
.WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0)));
// mock fetch transactions
EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger).Times(1);
TransactionAndMetadata t1;
t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30).getSerializer().peekData();
t1.ledgerSequence = SEQ;
ON_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(SEQ, _))
.WillByDefault(Return(std::vector<TransactionAndMetadata>{t1}));
// setLastPublishedSequence not in strand, should verify before run
EXPECT_TRUE(publisher.getLastPublishedSequence());
EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
MockSubscriptionManager* rawSubscriptionManagerPtr =
dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 1)).Times(1);
EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
// mock 1 transaction
EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction).Times(1);
ctx.run();
// last publish time should be set
EXPECT_TRUE(publisher.lastPublishAgeSeconds() <= 1);
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqStopIsTrue)
{
SystemState dummyState;
dummyState.isStopping = true;
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
EXPECT_FALSE(publisher.publish(SEQ, {}));
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqMaxAttampt)
{
SystemState dummyState;
dummyState.isStopping = false;
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
static auto constexpr MAX_ATTEMPT = 2;
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
EXPECT_CALL(*rawBackendPtr, hardFetchLedgerRange).Times(MAX_ATTEMPT);
LedgerRange const range{.minSequence = SEQ - 1, .maxSequence = SEQ - 1};
ON_CALL(*rawBackendPtr, hardFetchLedgerRange(_)).WillByDefault(Return(range));
EXPECT_FALSE(publisher.publish(SEQ, MAX_ATTEMPT));
}
TEST_F(ETLLedgerPublisherTest, PublishLedgerSeqStopIsFalse)
{
SystemState dummyState;
dummyState.isStopping = false;
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
LedgerRange const range{.minSequence = SEQ, .maxSequence = SEQ};
ON_CALL(*rawBackendPtr, hardFetchLedgerRange(_)).WillByDefault(Return(range));
EXPECT_CALL(*rawBackendPtr, hardFetchLedgerRange).Times(1);
auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, AGE);
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(SEQ, _)).WillByDefault(Return(dummyLedgerInfo));
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
ON_CALL(*rawBackendPtr, fetchLedgerDiff(SEQ, _)).WillByDefault(Return(std::vector<LedgerObject>{}));
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(SEQ, _)).Times(1);
EXPECT_CALL(mockCache, updateImp).Times(1);
EXPECT_TRUE(publisher.publish(SEQ, {}));
ctx.run();
}
TEST_F(ETLLedgerPublisherTest, PublishMultipleTxInOrder)
{
SystemState dummyState;
dummyState.isWriting = true;
auto const dummyLedgerInfo = CreateLedgerInfo(LEDGERHASH, SEQ, 0); // age is 0
detail::LedgerPublisher publisher(ctx, mockBackendPtr, mockCache, mockSubscriptionManagerPtr, dummyState);
mockBackendPtr->updateRange(SEQ - 1);
mockBackendPtr->updateRange(SEQ);
publisher.publish(dummyLedgerInfo);
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
EXPECT_CALL(*rawBackendPtr, fetchLedgerDiff(_, _)).Times(0);
// mock fetch fee
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ripple::keylet::fees().key, SEQ, _))
.WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0)));
// mock fetch transactions
EXPECT_CALL(*rawBackendPtr, fetchAllTransactionsInLedger).Times(1);
// t1 index > t2 index
TransactionAndMetadata t1;
t1.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
t1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30, 2).getSerializer().peekData();
t1.ledgerSequence = SEQ;
t1.date = 1;
TransactionAndMetadata t2;
t2.transaction = CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 100, 3, SEQ).getSerializer().peekData();
t2.metadata = CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 110, 30, 1).getSerializer().peekData();
t2.ledgerSequence = SEQ;
t2.date = 2;
ON_CALL(*rawBackendPtr, fetchAllTransactionsInLedger(SEQ, _))
.WillByDefault(Return(std::vector<TransactionAndMetadata>{t1, t2}));
// setLastPublishedSequence not in strand, should verify before run
EXPECT_TRUE(publisher.getLastPublishedSequence());
EXPECT_EQ(publisher.getLastPublishedSequence().value(), SEQ);
MockSubscriptionManager* rawSubscriptionManagerPtr =
dynamic_cast<MockSubscriptionManager*>(mockSubscriptionManagerPtr.get());
EXPECT_CALL(*rawSubscriptionManagerPtr, pubLedger(_, _, fmt::format("{}-{}", SEQ - 1, SEQ), 2)).Times(1);
EXPECT_CALL(*rawSubscriptionManagerPtr, pubBookChanges).Times(1);
// should call pubTransaction t2 first (greater tx index)
Sequence const s;
EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction(t2, _)).InSequence(s);
EXPECT_CALL(*rawSubscriptionManagerPtr, pubTransaction(t1, _)).InSequence(s);
ctx.run();
// last publish time should be set
EXPECT_TRUE(publisher.lastPublishAgeSeconds() <= 1);
}

View File

@@ -85,7 +85,7 @@ generateTestValuesForParametersTest()
"TypeInvalid", "TypeInvalid",
R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type":"wrong"})", R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "type":"wrong"})",
"invalidParams", "invalidParams",
"Invalid parameters."}, "Invalid field 'type'."},
AccountObjectsParamTestCaseBundle{ AccountObjectsParamTestCaseBundle{
"LedgerHashInvalid", "LedgerHashInvalid",
R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":"1"})", R"({"account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":"1"})",

View File

@@ -47,7 +47,7 @@ struct AccountTxParamTestCaseBundle
std::string testJson; std::string testJson;
std::optional<std::string> expectedError; std::optional<std::string> expectedError;
std::optional<std::string> expectedErrorMessage; std::optional<std::string> expectedErrorMessage;
std::uint32_t apiVersion = 2; std::uint32_t apiVersion = 2u;
}; };
// parameterized test cases for parameters check // parameterized test cases for parameters check
@@ -353,10 +353,9 @@ TEST_P(AccountTxParameterTest, CheckParams)
mockBackendPtr->updateRange(MINSEQ); // min mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max mockBackendPtr->updateRange(MAXSEQ); // max
auto const testBundle = GetParam(); auto const testBundle = GetParam();
auto* rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get()); auto* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
std::cout << "Before parse" << std::endl; ASSERT_NE(rawBackendPtr, nullptr);
auto const req = json::parse(testBundle.testJson); auto const req = json::parse(testBundle.testJson);
std::cout << "After parse" << std::endl;
if (testBundle.expectedError.has_value()) if (testBundle.expectedError.has_value())
{ {
ASSERT_TRUE(testBundle.expectedErrorMessage.has_value()); ASSERT_TRUE(testBundle.expectedErrorMessage.has_value());
@@ -372,14 +371,6 @@ TEST_P(AccountTxParameterTest, CheckParams)
} }
else else
{ {
if (req.as_object().contains("ledger_hash"))
{
EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).WillOnce(testing::Return(ripple::LedgerHeader{}));
}
else if (req.as_object().contains("ledger_index"))
{
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).WillOnce(testing::Return(ripple::LedgerHeader{}));
}
EXPECT_CALL(*rawBackendPtr, fetchAccountTransactions); EXPECT_CALL(*rawBackendPtr, fetchAccountTransactions);
runSpawn([&, this](auto yield) { runSpawn([&, this](auto yield) {
@@ -656,7 +647,7 @@ TEST_F(RPCAccountTxHandlerTest, BinaryTrue)
"144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451"
"243869B38667CBD89DF3"); "243869B38667CBD89DF3");
EXPECT_FALSE(output->at("transactions").as_array()[0].as_object().contains("date")); EXPECT_FALSE(output->at("transactions").as_array()[0].as_object().contains("date"));
EXPECT_FALSE(output->at("transactions").as_array()[0].as_object().contains("inLedger"));
EXPECT_FALSE(output->as_object().contains("limit")); EXPECT_FALSE(output->as_object().contains("limit"));
}); });
} }
@@ -962,200 +953,204 @@ TEST_F(RPCAccountTxHandlerTest, TxLargerThanMaxSeq)
}); });
} }
TEST_F(RPCAccountTxHandlerTest, NFTTxs) TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v1)
{ {
auto const OUT = R"({ auto const OUT = R"({
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 10, "ledger_index_min": 10,
"ledger_index_max": 30, "ledger_index_max": 30,
"transactions": [ "transactions": [
{
"meta": {
"AffectedNodes":
[
{
"ModifiedNode":
{ {
"meta": { "FinalFields":
"AffectedNodes": {
"NFTokens":
[ [
{ {
"ModifiedNode": "NFToken":
{ {
"FinalFields": "NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
{ "URI": "7465737475726C"
"NFTokens":
[
{
"NFToken":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
"URI": "7465737475726C"
}
},
{
"NFToken":
{
"NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"URI": "7465737475726C"
}
}
]
},
"LedgerEntryType": "NFTokenPage",
"PreviousFields":
{
"NFTokens":
[
{
"NFToken":
{
"NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"URI": "7465737475726C"
}
}
]
}
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenTaxon": 123,
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenMint",
"hash": "C74463F49CFDCBEF3E9902672719918CDE5042DC7E7660BEBD1D1105C4B6DFF4",
"ledger_index": 11,
"date": 1
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode":
{
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenBuyOffer": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenAcceptOffer",
"hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF",
"ledger_index": 11,
"date": 2
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode": {
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
} }
}, },
{ {
"DeletedNode": "NFToken":
{ {
"FinalFields": "NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
{ "URI": "7465737475726C"
"NFTokenID": "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"LedgerEntryType": "NFTokenOffer"
} }
} }
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_ids":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
] ]
}, },
"tx": "LedgerEntryType": "NFTokenPage",
"PreviousFields":
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "NFTokens":
"Fee": "50",
"NFTokenOffers":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
],
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCancelOffer",
"hash": "9F82743EEB30065FB9CB92C61F0F064B5859C5A590FA811FAAAD9C988E5B47DB",
"ledger_index": 11,
"date": 3
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[ [
{ {
"CreatedNode": "NFToken":
{ {
"LedgerEntryType": "NFTokenOffer", "NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"LedgerIndex": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA" "URI": "7465737475726C"
} }
} }
], ]
"TransactionIndex": 0, }
"TransactionResult": "tesSUCCESS",
"offer_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "123",
"Fee": "50",
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCreateOffer",
"hash": "ECB1837EB7C7C0AC22ECDCCE59FDD4795C70E0B9D8F4E1C9A9408BB7EC75DA5C",
"ledger_index": 11,
"date": 4
},
"validated": true
} }
],
"validated": true,
"marker":
{
"ledger": 12,
"seq": 34
} }
})"; ],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenTaxon": 123,
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenMint",
"hash": "C74463F49CFDCBEF3E9902672719918CDE5042DC7E7660BEBD1D1105C4B6DFF4",
"ledger_index": 11,
"inLedger": 11,
"date": 1
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode":
{
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenBuyOffer": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenAcceptOffer",
"hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF",
"ledger_index": 11,
"inLedger": 11,
"date": 2
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode": {
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
}
},
{
"DeletedNode":
{
"FinalFields":
{
"NFTokenID": "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"LedgerEntryType": "NFTokenOffer"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_ids":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
]
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenOffers":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
],
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCancelOffer",
"hash": "9F82743EEB30065FB9CB92C61F0F064B5859C5A590FA811FAAAD9C988E5B47DB",
"ledger_index": 11,
"inLedger": 11,
"date": 3
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"CreatedNode":
{
"LedgerEntryType": "NFTokenOffer",
"LedgerIndex": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"offer_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "123",
"Fee": "50",
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCreateOffer",
"hash": "ECB1837EB7C7C0AC22ECDCCE59FDD4795C70E0B9D8F4E1C9A9408BB7EC75DA5C",
"ledger_index": 11,
"inLedger": 11,
"date": 4
},
"validated": true
}
],
"validated": true,
"marker":
{
"ledger": 12,
"seq": 34
}
})";
mockBackendPtr->updateRange(MINSEQ); // min mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max mockBackendPtr->updateRange(MAXSEQ); // max
MockBackend* rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get()); MockBackend* rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
@@ -1181,7 +1176,233 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs)
ACCOUNT, ACCOUNT,
-1, -1,
-1)); -1));
auto const output = handler.process(input, Context{yield}); auto const output = handler.process(input, Context{.yield = yield, .apiVersion = 1u});
ASSERT_TRUE(output);
EXPECT_EQ(*output, json::parse(OUT));
});
}
TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2)
{
auto const OUT = R"({
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": 10,
"ledger_index_max": 30,
"transactions": [
{
"meta": {
"AffectedNodes":
[
{
"ModifiedNode":
{
"FinalFields":
{
"NFTokens":
[
{
"NFToken":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
"URI": "7465737475726C"
}
},
{
"NFToken":
{
"NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"URI": "7465737475726C"
}
}
]
},
"LedgerEntryType": "NFTokenPage",
"PreviousFields":
{
"NFTokens":
[
{
"NFToken":
{
"NFTokenID": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"URI": "7465737475726C"
}
}
]
}
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenTaxon": 123,
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenMint",
"hash": "C74463F49CFDCBEF3E9902672719918CDE5042DC7E7660BEBD1D1105C4B6DFF4",
"ledger_index": 11,
"date": 1
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode":
{
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenBuyOffer": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenAcceptOffer",
"hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF",
"ledger_index": 11,
"date": 2
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"DeletedNode": {
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"LedgerEntryType": "NFTokenOffer"
}
},
{
"DeletedNode":
{
"FinalFields":
{
"NFTokenID": "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
},
"LedgerEntryType": "NFTokenOffer"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"nftoken_ids":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
]
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenOffers":
[
"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"
],
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCancelOffer",
"hash": "9F82743EEB30065FB9CB92C61F0F064B5859C5A590FA811FAAAD9C988E5B47DB",
"ledger_index": 11,
"date": 3
},
"validated": true
},
{
"meta":
{
"AffectedNodes":
[
{
"CreatedNode":
{
"LedgerEntryType": "NFTokenOffer",
"LedgerIndex": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"offer_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"tx":
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "123",
"Fee": "50",
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenCreateOffer",
"hash": "ECB1837EB7C7C0AC22ECDCCE59FDD4795C70E0B9D8F4E1C9A9408BB7EC75DA5C",
"ledger_index": 11,
"date": 4
},
"validated": true
}
],
"validated": true,
"marker":
{
"ledger": 12,
"seq": 34
}
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
MockBackend* rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
auto const transactions = genNFTTransactions(MINSEQ + 1);
auto const transCursor = TransactionsAndCursor{transactions, TransactionsCursor{12, 34}};
ON_CALL(*rawBackendPtr, fetchAccountTransactions).WillByDefault(Return(transCursor));
EXPECT_CALL(
*rawBackendPtr,
fetchAccountTransactions(
testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{10, 11})), testing::_))
.Times(1);
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}};
auto const static input = json::parse(fmt::format(
R"({{
"account": "{}",
"ledger_index_min": {},
"ledger_index_max": {},
"forward": false,
"marker": {{"ledger": 10, "seq": 11}}
}})",
ACCOUNT,
-1,
-1));
auto const output = handler.process(input, Context{.yield = yield, .apiVersion = 2u});
ASSERT_TRUE(output); ASSERT_TRUE(output);
EXPECT_EQ(*output, json::parse(OUT)); EXPECT_EQ(*output, json::parse(OUT));
}); });
@@ -1192,6 +1413,7 @@ struct AccountTxTransactionBundle
std::string testName; std::string testName;
std::string testJson; std::string testJson;
std::string result; std::string result;
std::uint32_t apiVersion = 2u;
}; };
// parameterized test cases for parameters check // parameterized test cases for parameters check
@@ -1351,7 +1573,57 @@ generateTransactionTypeTestValues()
})", })",
"[]"}, "[]"},
AccountTxTransactionBundle{ AccountTxTransactionBundle{
"Payment", "Payment_API_v1",
R"({
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated",
"tx_type": "Payment"
})",
R"([
{
"meta": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Balance": "22"
},
"LedgerEntryType": "AccountRoot"
}
},
{
"ModifiedNode": {
"FinalFields": {
"Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Balance": "23"
},
"LedgerEntryType": "AccountRoot"
}
}],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS",
"delivered_amount": "unavailable"
},
"tx": {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": "1",
"Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Fee": "1",
"Sequence": 32,
"SigningPubKey": "74657374",
"TransactionType": "Payment",
"hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2",
"ledger_index": 30,
"inLedger": 30,
"date": 1
},
"validated": true
}
])",
1u},
AccountTxTransactionBundle{
"Payment_API_v2",
R"({ R"({
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated", "ledger_index": "validated",
@@ -1397,7 +1669,8 @@ generateTransactionTypeTestValues()
}, },
"validated": true "validated": true
} }
])"}, ])",
2u},
AccountTxTransactionBundle{ AccountTxTransactionBundle{
"PaymentChannelClaim", "PaymentChannelClaim",
R"({ R"({
@@ -1484,7 +1757,7 @@ TEST_P(AccountTxTransactionTypeTest, SpecificTransactionType)
runSpawn([&, this](auto yield) { runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}}; auto const handler = AnyHandler{AccountTxHandler{mockBackendPtr}};
auto const req = json::parse(testBundle.testJson); auto const req = json::parse(testBundle.testJson);
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = testBundle.apiVersion});
EXPECT_TRUE(output); EXPECT_TRUE(output);
auto const transactions = output->at("transactions").as_array(); auto const transactions = output->at("transactions").as_array();

View File

@@ -256,7 +256,7 @@ generateParameterBookOffersTestBundles()
"taker": "123" "taker": "123"
})", })",
"invalidParams", "invalidParams",
"Invalid field 'taker'"}, "Invalid field 'taker'."},
ParameterTestBundle{ ParameterTestBundle{
"TakerNotString", "TakerNotString",
R"({ R"({
@@ -272,7 +272,7 @@ generateParameterBookOffersTestBundles()
"taker": 123 "taker": 123
})", })",
"invalidParams", "invalidParams",
"Invalid field 'taker'"}, "Invalid field 'taker'."},
ParameterTestBundle{ ParameterTestBundle{
"LimitNotInt", "LimitNotInt",
R"({ R"({
@@ -384,8 +384,7 @@ generateParameterBookOffersTestBundles()
} }
})", })",
"srcIsrMalformed", "srcIsrMalformed",
"Unneeded field 'taker_pays.issuer' for XRP currency " "Unneeded field 'taker_pays.issuer' for XRP currency specification."},
"specification."},
ParameterTestBundle{ ParameterTestBundle{
"PaysCurrencyWithXRPIssuer", "PaysCurrencyWithXRPIssuer",
R"({ R"({
@@ -430,8 +429,7 @@ generateParameterBookOffersTestBundles()
} }
})", })",
"dstIsrMalformed", "dstIsrMalformed",
"Unneeded field 'taker_gets.issuer' for XRP currency " "Unneeded field 'taker_gets.issuer' for XRP currency specification."},
"specification."},
ParameterTestBundle{ ParameterTestBundle{
"BadMarket", "BadMarket",
R"({ R"({

View File

@@ -103,7 +103,7 @@ generateTestValuesForParametersTest()
"invalidParams", "invalidParams",
"ledgerIndexMalformed"}, "ledgerIndexMalformed"},
ParamTestCaseBundle{"UnknownOption", R"({})", "unknownOption", "Unknown option."}, ParamTestCaseBundle{"UnknownOption", R"({})", "invalidParams", "Invalid parameters."},
ParamTestCaseBundle{ ParamTestCaseBundle{
"InvalidDepositPreauthType", "InvalidDepositPreauthType",
@@ -1094,8 +1094,8 @@ TEST_F(RPCLedgerEntryTest, InvalidEntryTypeVersion2)
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2}); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2});
ASSERT_FALSE(output); ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error()); auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "unknownOption"); EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Unknown option."); EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
}); });
} }
@@ -1107,7 +1107,7 @@ TEST_F(RPCLedgerEntryTest, InvalidEntryTypeVersion1)
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 1}); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 1});
ASSERT_FALSE(output); ASSERT_FALSE(output);
auto const err = rpc::makeError(output.error()); auto const err = rpc::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams"); EXPECT_EQ(err.at("error").as_string(), "unknownOption");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters."); EXPECT_EQ(err.at("error_message").as_string(), "Unknown option.");
}); });
} }

View File

@@ -410,8 +410,7 @@ generateTestValuesForParametersTest()
] ]
})", })",
"dstIsrMalformed", "dstIsrMalformed",
"Unneeded field 'taker_gets.issuer' for XRP currency " "Unneeded field 'taker_gets.issuer' for XRP currency specification."},
"specification."},
SubscribeParamTestCaseBundle{ SubscribeParamTestCaseBundle{
"BooksItemTakerPaysXRPHasIssuer", "BooksItemTakerPaysXRPHasIssuer",
R"({ R"({
@@ -430,8 +429,7 @@ generateTestValuesForParametersTest()
] ]
})", })",
"srcIsrMalformed", "srcIsrMalformed",
"Unneeded field 'taker_pays.issuer' for XRP currency " "Unneeded field 'taker_pays.issuer' for XRP currency specification."},
"specification."},
SubscribeParamTestCaseBundle{ SubscribeParamTestCaseBundle{
"BooksItemBadMartket", "BooksItemBadMartket",
R"({ R"({

View File

@@ -28,12 +28,49 @@ using namespace rpc;
namespace json = boost::json; namespace json = boost::json;
using namespace testing; using namespace testing;
constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; auto constexpr static TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD";
constexpr static auto NFTID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"; auto constexpr static NFTID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF";
constexpr static auto NFTID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"; auto constexpr static NFTID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA";
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; auto constexpr static ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; auto constexpr static ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; auto constexpr static CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000";
auto constexpr static DEFAULT_OUT = R"({
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "2",
"Sequence": 100,
"SigningPubKey": "74657374",
"TakerGets": {
"currency": "0158415500000000C1F76FF6ECB0BAC600000000",
"issuer": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"value": "200"
},
"TakerPays": "300",
"TransactionType": "OfferCreate",
"hash": "2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08",
"meta": {
"AffectedNodes": [
{
"CreatedNode": {
"LedgerEntryType": "Offer",
"NewFields": {
"TakerGets": "200",
"TakerPays": {
"currency": "0158415500000000C1F76FF6ECB0BAC600000000",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "300"
}
}
}
}
],
"TransactionIndex": 100,
"TransactionResult": "tesSUCCESS"
},
"date": 123456,
"ledger_index": 100,
"validated": true
})";
class RPCTxTest : public HandlerBaseTest class RPCTxTest : public HandlerBaseTest
{ {
@@ -48,8 +85,8 @@ TEST_F(RPCTxTest, ExcessiveLgrRange)
"command": "tx", "command": "tx",
"transaction": "{}", "transaction": "{}",
"min_ledger": 1, "min_ledger": 1,
"max_ledger":1002 "max_ledger": 1002
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output); ASSERT_FALSE(output);
@@ -70,7 +107,7 @@ TEST_F(RPCTxTest, InvalidLgrRange)
"transaction": "{}", "transaction": "{}",
"max_ledger": 1, "max_ledger": 1,
"min_ledger": 10 "min_ledger": 10
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output); ASSERT_FALSE(output);
@@ -93,7 +130,7 @@ TEST_F(RPCTxTest, TxnNotFound)
R"({{ R"({{
"command": "tx", "command": "tx",
"transaction": "{}" "transaction": "{}"
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output); ASSERT_FALSE(output);
@@ -119,8 +156,8 @@ TEST_F(RPCTxTest, TxnNotFoundInGivenRangeSearchAllFalse)
"command": "tx", "command": "tx",
"transaction": "{}", "transaction": "{}",
"min_ledger": 1, "min_ledger": 1,
"max_ledger":1000 "max_ledger": 1000
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output); ASSERT_FALSE(output);
@@ -147,8 +184,8 @@ TEST_F(RPCTxTest, TxnNotFoundInGivenRangeSearchAllTrue)
"command": "tx", "command": "tx",
"transaction": "{}", "transaction": "{}",
"min_ledger": 1, "min_ledger": 1,
"max_ledger":1000 "max_ledger": 1000
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_FALSE(output); ASSERT_FALSE(output);
@@ -160,75 +197,77 @@ TEST_F(RPCTxTest, TxnNotFoundInGivenRangeSearchAllTrue)
}); });
} }
TEST_F(RPCTxTest, DefaultParameter) TEST_F(RPCTxTest, DefaultParameter_API_v1)
{ {
auto constexpr static OUT = R"({ auto const rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", ASSERT_NE(rawBackendPtr, nullptr);
"Fee":"2",
"Sequence":100,
"SigningPubKey":"74657374",
"TakerGets":{
"currency":"0158415500000000C1F76FF6ECB0BAC600000000",
"issuer":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"value":"200"
},
"TakerPays":"300",
"TransactionType":"OfferCreate",
"hash":"2E2FBAAFF767227FE4381C4BE9855986A6B9F96C62F6E443731AB36F7BBB8A08",
"meta":{
"AffectedNodes":[
{
"CreatedNode":{
"LedgerEntryType":"Offer",
"NewFields":{
"TakerGets":"200",
"TakerPays":{
"currency":"0158415500000000C1F76FF6ECB0BAC600000000",
"issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value":"300"
}
}
}
}
],
"TransactionIndex":100,
"TransactionResult":"tesSUCCESS"
},
"date":123456,
"ledger_index":100,
"validated": true
})";
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
TransactionAndMetadata tx; TransactionAndMetadata tx;
tx.metadata = CreateMetaDataForCreateOffer(CURRENCY, ACCOUNT, 100, 200, 300).getSerializer().peekData(); tx.metadata = CreateMetaDataForCreateOffer(CURRENCY, ACCOUNT, 100, 200, 300).getSerializer().peekData();
tx.transaction = tx.transaction =
CreateCreateOfferTransactionObject(ACCOUNT, 2, 100, CURRENCY, ACCOUNT2, 200, 300).getSerializer().peekData(); CreateCreateOfferTransactionObject(ACCOUNT, 2, 100, CURRENCY, ACCOUNT2, 200, 300).getSerializer().peekData();
tx.date = 123456; tx.date = 123456;
tx.ledgerSequence = 100; tx.ledgerSequence = 100;
ON_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillByDefault(Return(tx)); ON_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillByDefault(Return(tx));
EXPECT_CALL(*rawBackendPtr, fetchTransaction).Times(1); EXPECT_CALL(*rawBackendPtr, fetchTransaction).Times(1);
runSpawn([this](auto yield) { runSpawn([this](auto yield) {
auto const handler = AnyHandler{TxHandler{mockBackendPtr}}; auto const handler = AnyHandler{TxHandler{mockBackendPtr}};
auto const req = json::parse(fmt::format( auto const req = json::parse(fmt::format(
R"({{ R"({{
"command": "tx", "command": "tx",
"transaction": "{}" "transaction": "{}"
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 1u});
ASSERT_TRUE(output); ASSERT_TRUE(output);
EXPECT_EQ(*output, json::parse(OUT));
auto v1Output = json::parse(DEFAULT_OUT);
v1Output.as_object()[JS(inLedger)] = v1Output.as_object()[JS(ledger_index)];
EXPECT_EQ(*output, v1Output);
});
}
TEST_F(RPCTxTest, DefaultParameter_API_v2)
{
auto const rawBackendPtr = dynamic_cast<MockBackend*>(mockBackendPtr.get());
ASSERT_NE(rawBackendPtr, nullptr);
TransactionAndMetadata tx;
tx.metadata = CreateMetaDataForCreateOffer(CURRENCY, ACCOUNT, 100, 200, 300).getSerializer().peekData();
tx.transaction =
CreateCreateOfferTransactionObject(ACCOUNT, 2, 100, CURRENCY, ACCOUNT2, 200, 300).getSerializer().peekData();
tx.date = 123456;
tx.ledgerSequence = 100;
ON_CALL(*rawBackendPtr, fetchTransaction(ripple::uint256{TXNID}, _)).WillByDefault(Return(tx));
EXPECT_CALL(*rawBackendPtr, fetchTransaction).Times(1);
runSpawn([this](auto yield) {
auto const handler = AnyHandler{TxHandler{mockBackendPtr}};
auto const req = json::parse(fmt::format(
R"({{
"command": "tx",
"transaction": "{}"
}})",
TXNID));
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2u});
ASSERT_TRUE(output);
EXPECT_EQ(*output, json::parse(DEFAULT_OUT));
}); });
} }
TEST_F(RPCTxTest, ReturnBinary) TEST_F(RPCTxTest, ReturnBinary)
{ {
// Note: `inLedger` is API v1 only. See DefaultOutput_*
auto constexpr static OUT = R"({ auto constexpr static OUT = R"({
"meta":"201C00000064F8E311006FE864D50AA87BEE5380000158415500000000C1F76FF6ECB0BAC6000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA96540000000000000C8E1E1F1031000", "meta": "201C00000064F8E311006FE864D50AA87BEE5380000158415500000000C1F76FF6ECB0BAC6000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA96540000000000000C8E1E1F1031000",
"tx":"120007240000006464400000000000012C65D5071AFD498D00000158415500000000C1F76FF6ECB0BAC600000000D31252CF902EF8DD8451243869B38667CBD89DF368400000000000000273047465737481144B4E9C06F24296074F7BC48F92A97916C6DC5EA9", "tx": "120007240000006464400000000000012C65D5071AFD498D00000158415500000000C1F76FF6ECB0BAC600000000D31252CF902EF8DD8451243869B38667CBD89DF368400000000000000273047465737481144B4E9C06F24296074F7BC48F92A97916C6DC5EA9",
"hash":"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD", "hash": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD",
"date":123456, "date": 123456,
"ledger_index":100, "ledger_index": 100,
"inLedger": 100,
"validated": true "validated": true
})"; })";
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get()); auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
@@ -247,7 +286,7 @@ TEST_F(RPCTxTest, ReturnBinary)
"command": "tx", "command": "tx",
"transaction": "{}", "transaction": "{}",
"binary": true "binary": true
}})", }})",
TXNID)); TXNID));
auto const output = handler.process(req, Context{yield}); auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output); ASSERT_TRUE(output);
@@ -257,6 +296,7 @@ TEST_F(RPCTxTest, ReturnBinary)
TEST_F(RPCTxTest, MintNFT) TEST_F(RPCTxTest, MintNFT)
{ {
// Note: `inLedger` is API v1 only. See DefaultOutput_*
auto const static OUT = fmt::format( auto const static OUT = fmt::format(
R"({{ R"({{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -307,9 +347,10 @@ TEST_F(RPCTxTest, MintNFT)
"TransactionResult": "tesSUCCESS", "TransactionResult": "tesSUCCESS",
"nftoken_id": "{}" "nftoken_id": "{}"
}}, }},
"validated": true,
"date": 123456, "date": 123456,
"ledger_index": 100 "ledger_index": 100,
"inLedger": 100,
"validated": true
}})", }})",
NFTID, NFTID,
NFTID); NFTID);

View File

@@ -405,8 +405,7 @@ generateTestValuesForParametersTest()
] ]
})", })",
"dstIsrMalformed", "dstIsrMalformed",
"Unneeded field 'taker_gets.issuer' for XRP currency " "Unneeded field 'taker_gets.issuer' for XRP currency specification."},
"specification."},
UnsubscribeParamTestCaseBundle{ UnsubscribeParamTestCaseBundle{
"BooksItemTakerPaysXRPHasIssuer", "BooksItemTakerPaysXRPHasIssuer",
R"({ R"({
@@ -426,8 +425,7 @@ generateTestValuesForParametersTest()
] ]
})", })",
"srcIsrMalformed", "srcIsrMalformed",
"Unneeded field 'taker_pays.issuer' for XRP currency " "Unneeded field 'taker_pays.issuer' for XRP currency specification."},
"specification."},
UnsubscribeParamTestCaseBundle{ UnsubscribeParamTestCaseBundle{
"BooksItemBadMartket", "BooksItemBadMartket",
R"({ R"({

View File

@@ -106,7 +106,8 @@ CreatePaymentTransactionMetaObject(
std::string_view accountId1, std::string_view accountId1,
std::string_view accountId2, std::string_view accountId2,
int finalBalance1, int finalBalance1,
int finalBalance2) int finalBalance2,
uint32_t transactionIndex)
{ {
ripple::STObject finalFields(ripple::sfFinalFields); ripple::STObject finalFields(ripple::sfFinalFields);
finalFields.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId1)); finalFields.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId1));
@@ -128,7 +129,7 @@ CreatePaymentTransactionMetaObject(
metaArray.push_back(node2); metaArray.push_back(node2);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0); metaObj.setFieldU32(ripple::sfTransactionIndex, transactionIndex);
return metaObj; return metaObj;
} }

View File

@@ -75,7 +75,8 @@ CreatePaymentTransactionMetaObject(
std::string_view accountId1, std::string_view accountId1,
std::string_view accountId2, std::string_view accountId2,
int finalBalance1, int finalBalance1,
int finalBalance2); int finalBalance2,
uint32_t transactionIndex = 0);
/* /*
* Create an account root ledger object * Create an account root ledger object

View File

@@ -549,7 +549,7 @@ TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableNotArray)
EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request);
} }
TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableEmptyArray) TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableArrayWithDigit)
{ {
static auto constexpr response = "params unparseable"; static auto constexpr response = "params unparseable";
@@ -558,7 +558,7 @@ TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparseableEmptyArray)
static auto constexpr requestJSON = R"({ static auto constexpr requestJSON = R"({
"method": "ledger", "method": "ledger",
"params": [] "params": [1]
})"; })";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);