diff --git a/CMakeLists.txt b/CMakeLists.txt index d975010f..f5268e1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,8 @@ target_sources(clio PRIVATE ## NextGen RPC src/rpc/common/Specs.cpp src/rpc/common/Validators.cpp + ## NextGen RPC handler + src/rpc/ngHandlers/AccountChannels.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -115,7 +117,8 @@ if(BUILD_TESTS) unittests/rpc/RPCHelpersTest.cpp unittests/rpc/handlers/TestHandlerTests.cpp unittests/rpc/handlers/DefaultProcessorTests.cpp - unittests/rpc/handlers/PingTest.cpp) + unittests/rpc/handlers/PingTest.cpp + unittests/rpc/handlers/AccountChannelsTest.cpp) include(CMake/deps/gtest.cmake) # if CODE_COVERAGE enable, add clio_test-ccov diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 6608636d..3970620d 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -102,11 +102,7 @@ getRequiredUInt(boost::json::object const& request, std::string const& field) } std::optional -parseAccountCursor( - BackendInterface const& backend, - std::uint32_t seq, - std::optional jsonCursor, - boost::asio::yield_context& yield) +parseAccountCursor(std::optional jsonCursor) { ripple::uint256 cursorIndex = beast::zero; std::uint64_t startHint = 0; @@ -596,6 +592,39 @@ ledgerInfoFromRequest(Context const& ctx) return *lgrInfo; } +// extract ledgerInfoFromRequest's parameter from context +std::variant +getLedgerInfoFromHashOrSeq( + BackendInterface const& backend, + boost::asio::yield_context& yield, + std::optional ledgerHash, + std::optional ledgerIndex, + uint32_t maxSeq) +{ + std::optional lgrInfo; + auto const err = + RPC::Status{RPC::RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"}; + if (ledgerHash) + { + lgrInfo = + backend.fetchLedgerByHash(ripple::uint256{*ledgerHash}, yield); + if (!lgrInfo || lgrInfo->seq > maxSeq) + return err; + + return *lgrInfo; + } + auto const ledgerSequence = ledgerIndex.value_or(maxSeq); + // return without check db + if (ledgerSequence > maxSeq) + return err; + + lgrInfo = backend.fetchLedgerBySequence(ledgerSequence, yield); + if (!lgrInfo) + return err; + + return *lgrInfo; +} + std::vector ledgerInfoToBlob(ripple::LedgerInfo const& info, bool includeHash) { @@ -646,7 +675,7 @@ traverseOwnedNodes( ripple::keylet::account(accountID).key, sequence, yield)) return Status{RippledError::rpcACT_NOT_FOUND}; - auto maybeCursor = parseAccountCursor(backend, sequence, jsonCursor, yield); + auto const maybeCursor = parseAccountCursor(jsonCursor); if (!maybeCursor) return Status(ripple::rpcINVALID_PARAMS, "Malformed cursor"); @@ -664,6 +693,32 @@ traverseOwnedNodes( atOwnedNode); } +std::variant +ngTraverseOwnedNodes( + BackendInterface const& backend, + ripple::AccountID const& accountID, + std::uint32_t sequence, + std::uint32_t limit, + std::optional jsonCursor, + boost::asio::yield_context& yield, + std::function atOwnedNode) +{ + auto const maybeCursor = parseAccountCursor(jsonCursor); + // the format is checked in RPC framework level + auto const [hexCursor, startHint] = *maybeCursor; + + return traverseOwnedNodes( + backend, + ripple::keylet::ownerDir(accountID), + hexCursor, + startHint, + sequence, + limit, + jsonCursor, + yield, + atOwnedNode); +} + std::variant traverseOwnedNodes( BackendInterface const& backend, diff --git a/src/rpc/RPCHelpers.h b/src/rpc/RPCHelpers.h index 422c32d2..f30ebe7d 100644 --- a/src/rpc/RPCHelpers.h +++ b/src/rpc/RPCHelpers.h @@ -50,11 +50,7 @@ std::uint64_t getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID); std::optional -parseAccountCursor( - BackendInterface const& backend, - std::uint32_t seq, - std::optional jsonCursor, - boost::asio::yield_context& yield); +parseAccountCursor(std::optional jsonCursor); // TODO this function should probably be in a different file and namespace std::pair< @@ -106,6 +102,14 @@ generatePubLedgerMessage( std::variant ledgerInfoFromRequest(Context const& ctx); +std::variant +getLedgerInfoFromHashOrSeq( + BackendInterface const& backend, + boost::asio::yield_context& yield, + std::optional ledgerHash, + std::optional ledgerIndex, + uint32_t maxSeq); + std::variant traverseOwnedNodes( BackendInterface const& backend, @@ -128,6 +132,18 @@ traverseOwnedNodes( boost::asio::yield_context& yield, std::function atOwnedNode); +// Remove the account check from traverseOwnedNodes +// Account check has been done by framework,remove it from internal function +std::variant +ngTraverseOwnedNodes( + BackendInterface const& backend, + ripple::AccountID const& accountID, + std::uint32_t sequence, + std::uint32_t limit, + std::optional jsonCursor, + boost::asio::yield_context& yield, + std::function atOwnedNode); + std::shared_ptr read( ripple::Keylet const& keylet, diff --git a/src/rpc/common/Concepts.h b/src/rpc/common/Concepts.h index 67f41d1f..a20fe5ea 100644 --- a/src/rpc/common/Concepts.h +++ b/src/rpc/common/Concepts.h @@ -50,7 +50,7 @@ concept Requirement = requires(T a) { */ // clang-format off template -concept CoroutineProcess = requires(T a, typename T::Input in, typename T::Output out, boost::asio::yield_context* y) { +concept CoroutineProcess = requires(T a, typename T::Input in, typename T::Output out, boost::asio::yield_context& y) { { a.process(in, y) } -> std::same_as>; }; template diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index a2e2f5a5..7d8de851 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include @@ -126,4 +127,42 @@ CustomValidator LedgerIndexValidator = CustomValidator{ return MaybeError{}; }}; +CustomValidator AccountValidator = CustomValidator{ + [](boost::json::value const& value, std::string_view key) -> MaybeError { + if (!value.is_string()) + { + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + std::string(key) + "NotString"}}; + } + // TODO: we are using accountFromStringStrict from RPCHelpers, after we + // 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 MaybeError{}; + }}; + +CustomValidator MarkerValidator = CustomValidator{ + [](boost::json::value const& value, std::string_view key) -> MaybeError { + if (!value.is_string()) + { + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + std::string(key) + "NotString"}}; + } + // TODO: we are using parseAccountCursor from RPCHelpers, after we + // remove all old handler, this function can be moved to here + if (!RPC::parseAccountCursor(value.as_string().c_str())) + { + // align with the current error message + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, "Malformed cursor"}}; + } + return MaybeError{}; + }}; + } // namespace RPCng::validation diff --git a/src/rpc/common/Validators.h b/src/rpc/common/Validators.h index 603b2173..e59a15da 100644 --- a/src/rpc/common/Validators.h +++ b/src/rpc/common/Validators.h @@ -360,4 +360,17 @@ extern CustomValidator LedgerIndexValidator; */ extern CustomValidator LedgerHashValidator; +/** + * @brief Provide a common used validator for account + * Account must be a string and the converted public key is valid + */ +extern CustomValidator AccountValidator; + +/** + * @brief Provide a common used validator for marker + * Marker is composed of a comma separated index and start hint. The + * former will be read as hex, and the latter can cast to uint64. + */ +extern CustomValidator MarkerValidator; + } // namespace RPCng::validation diff --git a/src/rpc/common/impl/Processors.h b/src/rpc/common/impl/Processors.h index 9f4c77d5..a5a20e3a 100644 --- a/src/rpc/common/impl/Processors.h +++ b/src/rpc/common/impl/Processors.h @@ -57,7 +57,7 @@ struct DefaultProcessor final } else { - auto const ret = handler.process(inData, ptrYield); + auto const ret = handler.process(inData, *ptrYield); // real handler is given expected Input, not json if (!ret) return Error{ret.error()}; // forward Status diff --git a/src/rpc/ngHandlers/AccountChannels.cpp b/src/rpc/ngHandlers/AccountChannels.cpp new file mode 100644 index 00000000..d5900348 --- /dev/null +++ b/src/rpc/ngHandlers/AccountChannels.cpp @@ -0,0 +1,210 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace RPCng { + +void +AccountChannelsHandler::addChannel( + std::vector& jsonChannels, + ripple::SLE const& channelSle) const +{ + ChannelResponse channel; + channel.channelID = ripple::to_string(channelSle.key()); + channel.account = + ripple::to_string(channelSle.getAccountID(ripple::sfAccount)); + channel.accountDestination = + ripple::to_string(channelSle.getAccountID(ripple::sfDestination)); + channel.amount = channelSle[ripple::sfAmount].getText(); + channel.balance = channelSle[ripple::sfBalance].getText(); + if (publicKeyType(channelSle[ripple::sfPublicKey])) + { + ripple::PublicKey const pk(channelSle[ripple::sfPublicKey]); + channel.publicKey = toBase58(ripple::TokenType::AccountPublic, pk); + channel.publicKeyHex = strHex(pk); + } + channel.settleDelay = channelSle[ripple::sfSettleDelay]; + if (auto const& v = channelSle[~ripple::sfExpiration]) + channel.expiration = *v; + if (auto const& v = channelSle[~ripple::sfCancelAfter]) + channel.cancelAfter = *v; + if (auto const& v = channelSle[~ripple::sfSourceTag]) + channel.sourceTag = *v; + if (auto const& v = channelSle[~ripple::sfDestinationTag]) + channel.destinationTag = *v; + + jsonChannels.push_back(channel); +} + +AccountChannelsHandler::Result +AccountChannelsHandler::process( + AccountChannelsHandler::Input input, + boost::asio::yield_context& yield) const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq( + *sharedPtrBackend_, + yield, + input.ledgerHash, + input.ledgerIndex, + range->maxSequence); + + if (auto status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + + // no need to check the return value, validator check for us + auto const accountID = RPC::accountFromStringStrict(input.account); + + auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( + ripple::keylet::account(*accountID).key, lgrInfo.seq, yield); + if (!accountLedgerObject) + return Error{RPC::Status{ + RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + + auto const destAccountID = input.destinationAccount + ? RPC::accountFromStringStrict(input.destinationAccount.value()) + : std::optional{}; + + Output response; + auto const addToResponse = [&](ripple::SLE&& sle) { + if (sle.getType() == ripple::ltPAYCHAN && + sle.getAccountID(ripple::sfAccount) == accountID && + (!destAccountID || + *destAccountID == sle.getAccountID(ripple::sfDestination))) + { + addChannel(response.channels, sle); + } + return true; + }; + + auto const next = RPC::ngTraverseOwnedNodes( + *sharedPtrBackend_, + *accountID, + lgrInfo.seq, + input.limit, + input.marker, + yield, + addToResponse); + + response.account = input.account; + response.limit = input.limit; + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + + auto const nextMarker = std::get(next); + if (nextMarker.isNonZero()) + response.marker = nextMarker.toString(); + + return response; +} + +AccountChannelsHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + AccountChannelsHandler::Input input; + input.account = jv.at("account").as_string().c_str(); + if (jsonObject.contains("limit")) + { + input.limit = jv.at("limit").as_int64(); + } + if (jsonObject.contains("marker")) + { + input.marker = jv.at("marker").as_string().c_str(); + } + if (jsonObject.contains("ledger_hash")) + { + input.ledgerHash = jv.at("ledger_hash").as_string().c_str(); + } + if (jsonObject.contains("destination_account")) + { + input.destinationAccount = + jv.at("destination_account").as_string().c_str(); + } + if (jsonObject.contains("ledger_index")) + { + if (!jsonObject.at("ledger_index").is_string()) + { + input.ledgerIndex = jv.at("ledger_index").as_int64(); + } + else if (jsonObject.at("ledger_index").as_string() != "validated") + { + input.ledgerIndex = + std::stoi(jv.at("ledger_index").as_string().c_str()); + } + } + + return input; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountChannelsHandler::Output output) +{ + boost::json::object obj; + obj = { + {"account", output.account}, + {"ledger_hash", output.ledgerHash}, + {"ledger_index", output.ledgerIndex}, + {"validated", output.validated}, + {"limit", output.limit}, + {"channels", boost::json::value_from(output.channels)}}; + if (output.marker) + obj["marker"] = output.marker.value(); + jv = obj; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountChannelsHandler::ChannelResponse channel) +{ + boost::json::object obj; + obj = { + {"channel_id", channel.channelID}, + {"account", channel.account}, + {"account_destination", channel.accountDestination}, + {"amount", channel.amount}, + {"balance", channel.balance}, + {"settle_delay", channel.settleDelay}}; + if (channel.publicKey) + obj["public_key"] = *(channel.publicKey); + if (channel.publicKeyHex) + obj["public_key_hex"] = *(channel.publicKeyHex); + if (channel.expiration) + obj["expiration"] = *(channel.expiration); + if (channel.cancelAfter) + obj["cancel_after"] = *(channel.cancelAfter); + if (channel.sourceTag) + obj["source_tag"] = *(channel.sourceTag); + if (channel.destinationTag) + obj["destination_tag"] = *(channel.destinationTag); + jv = obj; +} +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountChannels.h b/src/rpc/ngHandlers/AccountChannels.h new file mode 100644 index 00000000..41193167 --- /dev/null +++ b/src/rpc/ngHandlers/AccountChannels.h @@ -0,0 +1,125 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace RPCng { +class AccountChannelsHandler +{ + // dependencies + std::shared_ptr const sharedPtrBackend_; + +public: + // type align with SField.h + struct ChannelResponse + { + std::string channelID; + std::string account; + std::string accountDestination; + std::string amount; + std::string balance; + std::optional publicKey; + std::optional publicKeyHex; + uint32_t settleDelay; + std::optional expiration; + std::optional cancelAfter; + std::optional sourceTag; + std::optional destinationTag; + }; + + struct Output + { + std::vector channels; + std::string account; + std::string ledgerHash; + uint32_t ledgerIndex; + // validated should be sent via framework + bool validated = true; + uint32_t limit; + std::optional marker; + }; + + struct Input + { + std::string account; + std::optional destinationAccount; + std::optional ledgerHash; + std::optional ledgerIndex; + uint32_t limit = 50; + std::optional marker; + }; + + using Result = RPCng::HandlerReturnType; + + AccountChannelsHandler(std::shared_ptr& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + // clang-format off + static const RpcSpec rpcSpec = { + {"account", validation::Required{}, validation::AccountValidator}, + {"destination_account", validation::Type{},validation::AccountValidator}, + {"ledger_hash", validation::LedgerHashValidator}, + {"limit", validation::Type{},validation::Between{10,400}}, + {"ledger_index", validation::LedgerIndexValidator}, + {"marker", validation::MarkerValidator} + }; + // clang-format on + + return rpcSpec; + } + + Result + process(Input input, boost::asio::yield_context& yield) const; + +private: + void + addChannel(std::vector& jsonLines, ripple::SLE const& line) + const; +}; + +AccountChannelsHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountChannelsHandler::Output output); + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountChannelsHandler::ChannelResponse channel); +} // namespace RPCng diff --git a/unittests/rpc/BaseTests.cpp b/unittests/rpc/BaseTests.cpp index 798f06cd..be5e794e 100644 --- a/unittests/rpc/BaseTests.cpp +++ b/unittests/rpc/BaseTests.cpp @@ -280,3 +280,46 @@ TEST_F(RPCBaseTest, LedgerIndexValidator) ASSERT_FALSE(err); ASSERT_EQ(err.error().message, "ledgerIndexMalformed"); } + +TEST_F(RPCBaseTest, AccountValidator) +{ + auto spec = RpcSpec{ + {"account", AccountValidator}, + }; + auto failingInput = json::parse(R"({ "account": 256 })"); + ASSERT_FALSE(spec.validate(failingInput)); + + failingInput = + json::parse(R"({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })"); + ASSERT_FALSE(spec.validate(failingInput)); + + failingInput = json::parse( + R"({ "account": "02000000000000000000000000000000000000000000000000000000000000000" })"); + ASSERT_FALSE(spec.validate(failingInput)); + + auto passingInput = + json::parse(R"({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + passingInput = json::parse( + R"({ "account": "020000000000000000000000000000000000000000000000000000000000000000" })"); + ASSERT_TRUE(spec.validate(passingInput)); +} + +TEST_F(RPCBaseTest, MarkerValidator) +{ + auto spec = RpcSpec{ + {"marker", MarkerValidator}, + }; + auto failingInput = json::parse(R"({ "marker": 256 })"); + ASSERT_FALSE(spec.validate(failingInput)); + + failingInput = json::parse(R"({ "marker": "testtest" })"); + ASSERT_FALSE(spec.validate(failingInput)); + + failingInput = json::parse(R"({ "marker": "ABAB1234:1H" })"); + ASSERT_FALSE(spec.validate(failingInput)); + + auto passingInput = json::parse(R"({ "account": "ABAB1234:123" })"); + ASSERT_TRUE(spec.validate(passingInput)); +} diff --git a/unittests/rpc/handlers/AccountChannelsTest.cpp b/unittests/rpc/handlers/AccountChannelsTest.cpp new file mode 100644 index 00000000..c742c481 --- /dev/null +++ b/unittests/rpc/handlers/AccountChannelsTest.cpp @@ -0,0 +1,893 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include + +using namespace RPCng; +namespace json = boost::json; +using namespace testing; + +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto ACCOUNT3 = "rB9BMzh27F3Q6a5FtGPDayQoCCEdiRdqcK"; +constexpr static auto INDEX1 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto INDEX2 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"; +constexpr static auto TXNID = + "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; + +class RPCAccountHandlerTest : public SyncAsioContextTest, public MockBackendTest +{ + void + SetUp() override + { + SyncAsioContextTest::SetUp(); + MockBackendTest::SetUp(); + } + void + TearDown() override + { + MockBackendTest::TearDown(); + SyncAsioContextTest::TearDown(); + } +}; + +TEST_F(RPCAccountHandlerTest, NonHexLedgerHash) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_hash": "xxx" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerHashMalformed"); + }); + ctx.run(); +} + +TEST_F(RPCAccountHandlerTest, NonStringLedgerHash) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_hash": 123 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerHashNotString"); + }); + ctx.run(); +} + +TEST_F(RPCAccountHandlerTest, InvalidLedgerIndexString) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_index": "notvalidated" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); + ctx.run(); +} + +TEST_F(RPCAccountHandlerTest, MarkerNotString) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker":9 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "markerNotString"); + }); + ctx.run(); +} + +// error case : invalid marker +// marker format is composed of a comma separated index and start hint. The +// former will be read as hex, and the latter using boost lexical cast. +TEST_F(RPCAccountHandlerTest, InvalidMarker) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker": "123invalid" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor"); + }); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker":401 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); + ctx.run(); +} + +// the limit is between 10 400 +TEST_F(RPCAccountHandlerTest, IncorrectLimit) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit":9 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit":401 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); +} + +// error case: account invalid format, length is incorrect +TEST_F(RPCAccountHandlerTest, AccountInvalidFormat) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" + })"); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountMalformed"); + }); + ctx.run(); +} + +// error case: account invalid format +TEST_F(RPCAccountHandlerTest, AccountNotString) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const input = json::parse(R"({ + "account": 12 + })"); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotString"); + }); + ctx.run(); +} + +// error case ledger non exist via hash +TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerHash) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // mock fetchLedgerByHash return empty + ON_CALL(*rawBackendPtr, fetchLedgerByHash) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); + ctx.run(); +} + +// error case ledger non exist via index +TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerIndex) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // mock fetchLedgerBySequence return empty + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": "4" + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); + ctx.run(); +} + +// error case ledger > max seq via hash +// idk why this case will happen in reality +TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerHash2) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // mock fetchLedgerByHash return ledger but seq is 31 > 30 + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 31); + ON_CALL(*rawBackendPtr, fetchLedgerByHash) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); + ctx.run(); +} + +// error case ledger > max seq via index +TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerIndex2) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // no need to check from db,call fetchLedgerBySequence 0 time + // differ from previous logic + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(0); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": "31" + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); + ctx.run(); +} + +// error case account not exist +TEST_F(RPCAccountHandlerTest, NonExistAccount) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerByHash) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return emtpy + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{AccountChannelsHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "actNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound"); + }); + ctx.run(); +} + +// normal case when only provide account +TEST_F(RPCAccountHandlerTest, DefaultParameterTest) +{ + constexpr static auto correntOutput = R"({ + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "limit":50, + "channels":[ + { + "channel_id":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "account_destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "amount":"100", + "balance":"10", + "settle_delay":32, + "public_key":"aBMxWrnPUnvwZPfsmTyVizxEGsGheAu3Tsn6oPRgyjgvd2NggFxz", + "public_key_hex":"020000000000000000000000000000000000000000000000000000000000000000" + }, + { + "channel_id":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322", + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "account_destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "amount":"100", + "balance":"10", + "settle_delay":32, + "public_key":"aBMxWrnPUnvwZPfsmTyVizxEGsGheAu3Tsn6oPRgyjgvd2NggFxz", + "public_key_hex":"020000000000000000000000000000000000000000000000000000000000000000" + } + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index containing 2 indexes + ripple::STObject ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // return two payment channel objects + std::vector bbs; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + bbs.push_back(channel1.getSerializer().peekData()); + bbs.push_back(channel1.getSerializer().peekData()); + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(json::parse(correntOutput), *output); + }); + ctx.run(); +} + +// normal case : limit is used +TEST_F(RPCAccountHandlerTest, UseLimit) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + std::vector indexes; + std::vector bbs; + + auto repetitions = 50; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + ripple::STObject channel = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + bbs.push_back(channel.getSerializer().peekData()); + } + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + // it should not appear in return marker,marker is the current page + ownerDir.setFieldU64(ripple::sfIndexNext, 99); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 20 + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + + EXPECT_EQ((*output).as_object().at("channels").as_array().size(), 20); + EXPECT_THAT( + (*output).as_object().at("marker").as_string().c_str(), + EndsWith(",0")); + }); + ctx.run(); +} + +// normal case : destination is used +TEST_F(RPCAccountHandlerTest, UseDestination) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + std::vector indexes; + std::vector bbs; + + // 10 pay channel to ACCOUNT2 + auto repetitions = 10; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + ripple::STObject channel = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + bbs.push_back(channel.getSerializer().peekData()); + } + + // 20 pay channel to ACCOUNT3 + repetitions = 20; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + ripple::STObject channel = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT3, 100, 10, 32, TXNID, 28); + bbs.push_back(channel.getSerializer().peekData()); + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 30, + "destination_account":"{}" + }})", + ACCOUNT, + ACCOUNT3)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("channels").as_array().size(), 20); + }); + ctx.run(); +} + +// normal case : but the lines is emtpy +TEST_F(RPCAccountHandlerTest, EmptyChannel) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + ripple::STObject ownerDir = CreateOwnerDirLedgerObject({}, INDEX1); + + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("channels").as_array().size(), 0); + }); + ctx.run(); +} + +// Return expiration cancel_offer source_tag destination_tag when available +TEST_F(RPCAccountHandlerTest, OptionalResponseField) +{ + constexpr static auto correntOutput = R"({ + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "limit":50, + "channels":[ + { + "channel_id":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321", + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "account_destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "amount":"100", + "balance":"10", + "settle_delay":32, + "public_key":"aBMxWrnPUnvwZPfsmTyVizxEGsGheAu3Tsn6oPRgyjgvd2NggFxz", + "public_key_hex":"020000000000000000000000000000000000000000000000000000000000000000", + "expiration": 100, + "cancel_after": 200, + "source_tag": 300, + "destination_tag": 400 + }, + { + "channel_id":"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322", + "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "account_destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "amount":"100", + "balance":"10", + "settle_delay":32, + "public_key":"aBMxWrnPUnvwZPfsmTyVizxEGsGheAu3Tsn6oPRgyjgvd2NggFxz", + "public_key_hex":"020000000000000000000000000000000000000000000000000000000000000000", + "expiration": 100, + "cancel_after": 200, + "source_tag": 300, + "destination_tag": 400 + } + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + ripple::STObject ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // return two payment channel objects + std::vector bbs; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + channel1.setFieldU32(ripple::sfExpiration, 100); + channel1.setFieldU32(ripple::sfCancelAfter, 200); + channel1.setFieldU32(ripple::sfSourceTag, 300); + channel1.setFieldU32(ripple::sfDestinationTag, 400); + bbs.push_back(channel1.getSerializer().peekData()); + bbs.push_back(channel1.getSerializer().peekData()); + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(json::parse(correntOutput), *output); + }); + ctx.run(); +} + +// normal case : test marker output correct +TEST_F(RPCAccountHandlerTest, MarkerOutput) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto ownerDirKk = ripple::keylet::ownerDir(account).key; + constexpr static auto nextPage = 99; + constexpr static auto limit = 15; + auto ownerDir2Kk = + ripple::keylet::page(ripple::keylet::ownerDir(account), nextPage).key; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + // owner dir contains 10 indexes + int objectsCount = 10; + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + objectsCount--; + } + // return 15 objects + objectsCount = 15; + while (objectsCount != 0) + { + bbs.push_back(channel1.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, nextPage); + // first page 's next page is 99 + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(ownerDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + ripple::STObject ownerDir2 = CreateOwnerDirLedgerObject(indexes, INDEX1); + // second page's next page is 0 + ownerDir2.setFieldU64(ripple::sfIndexNext, 0); + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ownerDir2Kk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir2.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": {} + }})", + ACCOUNT, + limit)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ( + (*output).as_object().at("marker").as_string().c_str(), + fmt::format("{},{}", INDEX1, nextPage)); + EXPECT_EQ((*output).as_object().at("channels").as_array().size(), 15); + }); + ctx.run(); +} + +// normal case : handler marker correctly +TEST_F(RPCAccountHandlerTest, MarkerInput) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + constexpr static auto nextPage = 99; + constexpr static auto limit = 15; + auto ownerDirKk = + ripple::keylet::page(ripple::keylet::ownerDir(account), nextPage).key; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + int objectsCount = limit; + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + bbs.push_back(channel1.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, 0); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(ownerDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": {}, + "marker": "{},{}" + }})", + ACCOUNT, + limit, + INDEX1, + nextPage)); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_TRUE((*output).as_object().if_contains("marker") == nullptr); + // the first item is the marker itself, so the result will have limit-1 + // items + EXPECT_EQ( + (*output).as_object().at("channels").as_array().size(), limit - 1); + }); + ctx.run(); +} diff --git a/unittests/rpc/handlers/impl/FakesAndMocks.h b/unittests/rpc/handlers/impl/FakesAndMocks.h index 59c6d2f4..7c71f9d0 100644 --- a/unittests/rpc/handlers/impl/FakesAndMocks.h +++ b/unittests/rpc/handlers/impl/FakesAndMocks.h @@ -122,7 +122,7 @@ public: } Result - process(Input input, boost::asio::yield_context* ptrYield) const + process(Input input, boost::asio::yield_context& yield) const { return Output{ input.hello + '_' + std::to_string(input.limit.value_or(0))};