From a3211f4458d162e742416086000052a0b53aea1e Mon Sep 17 00:00:00 2001 From: cyan317 <120398799+cindyyan317@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:17:51 +0000 Subject: [PATCH] Handler account_currencies (#524) Fixes #525 --- CMakeLists.txt | 2 + src/rpc/RPCHelpers.cpp | 6 +- src/rpc/ngHandlers/AccountChannels.h | 3 +- src/rpc/ngHandlers/AccountCurrencies.cpp | 129 ++++++++ src/rpc/ngHandlers/AccountCurrencies.h | 87 ++++++ .../rpc/handlers/AccountChannelsTest.cpp | 20 +- .../rpc/handlers/AccountCurrenciesTest.cpp | 295 ++++++++++++++++++ unittests/util/Fixtures.h | 20 ++ unittests/util/TestObject.cpp | 36 +++ unittests/util/TestObject.h | 13 + 10 files changed, 592 insertions(+), 19 deletions(-) create mode 100644 src/rpc/ngHandlers/AccountCurrencies.cpp create mode 100644 src/rpc/ngHandlers/AccountCurrencies.h create mode 100644 unittests/rpc/handlers/AccountCurrenciesTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f5268e1a..0aa04431 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ target_sources(clio PRIVATE src/rpc/common/Validators.cpp ## NextGen RPC handler src/rpc/ngHandlers/AccountChannels.cpp + src/rpc/ngHandlers/AccountCurrencies.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -116,6 +117,7 @@ if(BUILD_TESTS) unittests/rpc/BaseTests.cpp unittests/rpc/RPCHelpersTest.cpp unittests/rpc/handlers/TestHandlerTests.cpp + unittests/rpc/handlers/AccountCurrenciesTest.cpp unittests/rpc/handlers/DefaultProcessorTests.cpp unittests/rpc/handlers/PingTest.cpp unittests/rpc/handlers/AccountChannelsTest.cpp) diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 3970620d..a908eeab 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -606,8 +606,10 @@ getLedgerInfoFromHashOrSeq( RPC::Status{RPC::RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"}; if (ledgerHash) { - lgrInfo = - backend.fetchLedgerByHash(ripple::uint256{*ledgerHash}, yield); + // invoke uint256's constructor to parse the hex string , instead of + // copying buffer + ripple::uint256 ledgerHash256{std::string_view(*ledgerHash)}; + lgrInfo = backend.fetchLedgerByHash(ledgerHash256, yield); if (!lgrInfo || lgrInfo->seq > maxSeq) return err; diff --git a/src/rpc/ngHandlers/AccountChannels.h b/src/rpc/ngHandlers/AccountChannels.h index 41193167..4e79fbd3 100644 --- a/src/rpc/ngHandlers/AccountChannels.h +++ b/src/rpc/ngHandlers/AccountChannels.h @@ -75,7 +75,8 @@ public: using Result = RPCng::HandlerReturnType; - AccountChannelsHandler(std::shared_ptr& sharedPtrBackend) + AccountChannelsHandler( + std::shared_ptr const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend) { } diff --git a/src/rpc/ngHandlers/AccountCurrencies.cpp b/src/rpc/ngHandlers/AccountCurrencies.cpp new file mode 100644 index 00000000..4a8b7fad --- /dev/null +++ b/src/rpc/ngHandlers/AccountCurrencies.cpp @@ -0,0 +1,129 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { +AccountCurrenciesHandler::Result +AccountCurrenciesHandler::process( + AccountCurrenciesHandler::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 const status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + + 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"}}; + + Output response; + auto const addToResponse = [&](ripple::SLE&& sle) { + if (sle.getType() == ripple::ltRIPPLE_STATE) + { + ripple::STAmount balance = sle.getFieldAmount(ripple::sfBalance); + auto const lowLimit = sle.getFieldAmount(ripple::sfLowLimit); + auto const highLimit = sle.getFieldAmount(ripple::sfHighLimit); + bool const viewLowest = (lowLimit.getIssuer() == accountID); + auto const lineLimit = viewLowest ? lowLimit : highLimit; + auto const lineLimitPeer = !viewLowest ? lowLimit : highLimit; + if (!viewLowest) + balance.negate(); + if (balance < lineLimit) + response.receiveCurrencies.insert( + ripple::to_string(balance.getCurrency())); + if ((-balance) < lineLimitPeer) + response.sendCurrencies.insert( + ripple::to_string(balance.getCurrency())); + } + return true; + }; + + // traverse all owned nodes, limit->max, marker->empty + RPC::ngTraverseOwnedNodes( + *sharedPtrBackend_, + *accountID, + lgrInfo.seq, + std::numeric_limits::max(), + {}, + yield, + addToResponse); + + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + return response; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountCurrenciesHandler::Output output) +{ + jv = { + {"ledger_hash", output.ledgerHash}, + {"ledger_index", output.ledgerIndex}, + {"validated", output.validated}, + {"receive_currencies", + boost::json::value_from(output.receiveCurrencies)}, + {"send_currencies", boost::json::value_from(output.sendCurrencies)}}; +} + +AccountCurrenciesHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + AccountCurrenciesHandler::Input input; + input.account = jv.at("account").as_string().c_str(); + if (jsonObject.contains("ledger_hash")) + { + input.ledgerHash = jv.at("ledger_hash").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; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountCurrencies.h b/src/rpc/ngHandlers/AccountCurrencies.h new file mode 100644 index 00000000..3c3321f6 --- /dev/null +++ b/src/rpc/ngHandlers/AccountCurrencies.h @@ -0,0 +1,87 @@ +//------------------------------------------------------------------------------ +/* + 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 AccountCurrenciesHandler +{ + // dependencies + std::shared_ptr sharedPtrBackend_; + +public: + struct Output + { + std::string ledgerHash; + uint32_t ledgerIndex; + std::set receiveCurrencies; + std::set sendCurrencies; + // validated should be sent via framework + bool validated = true; + }; + + // TODO:we did not implement the "strict" field + struct Input + { + std::string account; + std::optional ledgerHash; + std::optional ledgerIndex; + }; + + using Result = RPCng::HandlerReturnType; + + AccountCurrenciesHandler( + std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static const RpcSpec rpcSpec = { + {"account", validation::Required{}, validation::AccountValidator}, + {"ledger_hash", validation::LedgerHashValidator}, + {"ledger_index", validation::LedgerIndexValidator}}; + return rpcSpec; + } + + Result + process(Input input, boost::asio::yield_context& yield) const; +}; + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountCurrenciesHandler::Output output); + +AccountCurrenciesHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); +} // namespace RPCng diff --git a/unittests/rpc/handlers/AccountChannelsTest.cpp b/unittests/rpc/handlers/AccountChannelsTest.cpp index c742c481..96006fe5 100644 --- a/unittests/rpc/handlers/AccountChannelsTest.cpp +++ b/unittests/rpc/handlers/AccountChannelsTest.cpp @@ -40,20 +40,8 @@ constexpr static auto INDEX2 = constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; -class RPCAccountHandlerTest : public SyncAsioContextTest, public MockBackendTest +class RPCAccountHandlerTest : public HandlerBaseTest { - void - SetUp() override - { - SyncAsioContextTest::SetUp(); - MockBackendTest::SetUp(); - } - void - TearDown() override - { - MockBackendTest::TearDown(); - SyncAsioContextTest::TearDown(); - } }; TEST_F(RPCAccountHandlerTest, NonHexLedgerHash) @@ -250,7 +238,7 @@ TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerHash) MockBackend* rawBackendPtr = static_cast(mockBackendPtr.get()); // mock fetchLedgerByHash return empty - ON_CALL(*rawBackendPtr, fetchLedgerByHash) + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) .WillByDefault(Return(std::optional{})); EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); @@ -311,7 +299,7 @@ TEST_F(RPCAccountHandlerTest, NonExistLedgerViaLedgerHash2) mockBackendPtr->updateRange(30); // max // mock fetchLedgerByHash return ledger but seq is 31 > 30 auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 31); - ON_CALL(*rawBackendPtr, fetchLedgerByHash) + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) .WillByDefault(Return(ledgerinfo)); EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); auto const input = json::parse(fmt::format( @@ -367,7 +355,7 @@ TEST_F(RPCAccountHandlerTest, NonExistAccount) mockBackendPtr->updateRange(10); // min mockBackendPtr->updateRange(30); // max auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); - ON_CALL(*rawBackendPtr, fetchLedgerByHash) + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) .WillByDefault(Return(ledgerinfo)); EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); // fetch account object return emtpy diff --git a/unittests/rpc/handlers/AccountCurrenciesTest.cpp b/unittests/rpc/handlers/AccountCurrenciesTest.cpp new file mode 100644 index 00000000..bda9e3fb --- /dev/null +++ b/unittests/rpc/handlers/AccountCurrenciesTest.cpp @@ -0,0 +1,295 @@ +//------------------------------------------------------------------------------ +/* + 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 ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto INDEX1 = + "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto INDEX2 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto TXNID = + "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"; + +class RPCAccountCurrenciesHandlerTest : public HandlerBaseTest +{ +}; + +TEST_F(RPCAccountCurrenciesHandlerTest, AccountNotExsit) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + 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(); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerNonExistViaSequence) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // return empty ledgerinfo + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(30, _)) + .WillByDefault(Return(std::optional{})); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + 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(); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerNonExistViaHash) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // return empty ledgerinfo + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(std::optional{})); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "ledger_hash":"{}" + }})", + ACCOUNT, + LEDGERHASH)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const output = handler.process(input); + 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(); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, DefaultParameter) +{ + auto constexpr static OUTPUT = R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "receive_currencies":[ + "EUR", + "JPY" + ], + "send_currencies":[ + "EUR", + "USD" + ] + })"; + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(30, _)) + .WillByDefault(Return(ledgerinfo)); + // return valid account + auto const accountKk = + ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, + ripple::uint256{INDEX2}, + ripple::uint256{INDEX2}}, + INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // ACCOUNT can receive USD 10 from ACCOUNT2 and send USD 20 to ACCOUNT2, now + // the balance is 100, ACCOUNT can only send USD to ACCOUNT2 + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123); + // ACCOUNT2 can receive JPY 10 from ACCOUNT and send JPY 20 to ACCOUNT, now + // the balance is 100, ACCOUNT2 can only send JPY to ACCOUNT + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, "JPY", ISSUER, 100, ACCOUNT2, 10, ACCOUNT, 20, TXNID, 123); + // ACCOUNT can receive EUR 10 from ACCOUNT and send EUR 20 to ACCOUNT2, now + // the balance is 8, ACCOUNT can receive/send EUR to/from ACCOUNT2 + auto const line3 = CreateRippleStateLedgerObject( + ACCOUNT, "EUR", ISSUER, 8, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123); + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + bbs.push_back(line3.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(OUTPUT)); + }); + ctx.run(); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderHash) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + // return valid account + auto const accountKk = + ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = + CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, 30, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123); + bbs.push_back(line1.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "ledger_hash":"{}" + }})", + ACCOUNT, + LEDGERHASH)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + }); + ctx.run(); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderSeq) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto const ledgerSeq = 29; + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, ledgerSeq); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(ledgerSeq, _)) + .WillByDefault(Return(ledgerinfo)); + // return valid account + auto const accountKk = + ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, ledgerSeq, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = + CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, ledgerSeq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123); + bbs.push_back(line1.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "ledger_index":{} + }})", + ACCOUNT, + ledgerSeq)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ( + (*output).as_object().at("ledger_index").as_uint64(), ledgerSeq); + }); + ctx.run(); +} diff --git a/unittests/util/Fixtures.h b/unittests/util/Fixtures.h index 5a9cea95..5cfa5249 100644 --- a/unittests/util/Fixtures.h +++ b/unittests/util/Fixtures.h @@ -177,3 +177,23 @@ struct MockBackendTest : virtual public NoLoggerFixture protected: std::shared_ptr mockBackendPtr; }; + +/** + * @brief Fixture with an mock backend and an embedded boost::asio context + * Handler unittest base class + */ +class HandlerBaseTest : public MockBackendTest, public SyncAsioContextTest +{ + void + SetUp() override + { + MockBackendTest::SetUp(); + SyncAsioContextTest::SetUp(); + } + void + TearDown() override + { + SyncAsioContextTest::TearDown(); + MockBackendTest::TearDown(); + } +}; diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index ed029cf5..9d4201c0 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -153,6 +153,12 @@ CreateCreateOfferTransactionObject( ripple::Issue GetIssue(std::string_view currency, std::string_view issuerId) { + // standard currency + if (currency.size() == 3) + return ripple::Issue( + ripple::to_currency(std::string(currency)), + ripple::parseBase58(std::string(issuerId)) + .value()); return ripple::Issue( ripple::Currency{currency}, ripple::parseBase58(std::string(issuerId)).value()); @@ -285,3 +291,33 @@ CreatePaymentChannelLedgerObject( channel.setFieldVL(ripple::sfPublicKey, slice); return channel; } + +[[nodiscard]] ripple::STObject +CreateRippleStateLedgerObject( + std::string_view accountId, + std::string_view currency, + std::string_view issuerId, + int balance, + std::string_view lowNodeAccountId, + int lowLimit, + std::string_view highNodeAccountId, + int highLimit, + std::string_view previousTxnId, + uint32_t previousTxnSeq) +{ + auto line = ripple::STObject(ripple::sfLedgerEntry); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldU32(ripple::sfFlags, 0); + line.setFieldAmount( + ripple::sfBalance, + ripple::STAmount(GetIssue(currency, issuerId), balance)); + line.setFieldAmount( + ripple::sfHighLimit, + ripple::STAmount(GetIssue(currency, highNodeAccountId), highLimit)); + line.setFieldAmount( + ripple::sfLowLimit, + ripple::STAmount(GetIssue(currency, lowNodeAccountId), lowLimit)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{previousTxnId}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxnSeq); + return line; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h index 0bf1a09b..8751744d 100644 --- a/unittests/util/TestObject.h +++ b/unittests/util/TestObject.h @@ -156,3 +156,16 @@ CreatePaymentChannelLedgerObject( uint32_t settleDelay, std::string_view previousTxnId, uint32_t previousTxnSeq); + +[[nodiscard]] ripple::STObject +CreateRippleStateLedgerObject( + std::string_view accountId, + std::string_view currency, + std::string_view issuerId, + int balance, + std::string_view lowNodeAccountId, + int lowLimit, + std::string_view highNodeAccountId, + int highLimit, + std::string_view previousTxnId, + uint32_t previousTxnSeq);