diff --git a/CMakeLists.txt b/CMakeLists.txt index 9247f751..8e31da3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/NFTSellOffers.cpp src/rpc/ngHandlers/AccountTx.cpp src/rpc/ngHandlers/AccountOffers.cpp + src/rpc/ngHandlers/AccountInfo.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -161,6 +162,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/NFTInfoTest.cpp unittests/rpc/handlers/NFTBuyOffersTest.cpp unittests/rpc/handlers/NFTSellOffersTest.cpp + unittests/rpc/handlers/AccountInfoTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/ngHandlers/AccountChannels.h b/src/rpc/ngHandlers/AccountChannels.h index 0d11b5af..841d5b47 100644 --- a/src/rpc/ngHandlers/AccountChannels.h +++ b/src/rpc/ngHandlers/AccountChannels.h @@ -86,7 +86,7 @@ public: spec() const { // clang-format off - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(account), validation::Required{}, validation::AccountValidator}, {JS(destination_account), validation::Type{},validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, diff --git a/src/rpc/ngHandlers/AccountCurrencies.h b/src/rpc/ngHandlers/AccountCurrencies.h index 9fbd18a3..ac141e06 100644 --- a/src/rpc/ngHandlers/AccountCurrencies.h +++ b/src/rpc/ngHandlers/AccountCurrencies.h @@ -64,7 +64,7 @@ public: RpcSpecConstRef spec() const { - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(account), validation::Required{}, validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}}; diff --git a/src/rpc/ngHandlers/AccountInfo.cpp b/src/rpc/ngHandlers/AccountInfo.cpp new file mode 100644 index 00000000..71a9cb1a --- /dev/null +++ b/src/rpc/ngHandlers/AccountInfo.cpp @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { +AccountInfoHandler::Result +AccountInfoHandler::process( + AccountInfoHandler::Input input, + boost::asio::yield_context& yield) const +{ + if (!input.account && !input.ident) + return Error{RPC::Status{RPC::RippledError::rpcACT_MALFORMED}}; + + 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 accountStr = input.account.value_or(input.ident.value_or("")); + auto const accountID = RPC::accountFromStringStrict(accountStr); + auto const accountKeylet = ripple::keylet::account(*accountID); + auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( + accountKeylet.key, lgrInfo.seq, yield); + if (!accountLedgerObject) + return Error{RPC::Status{ + RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{ + accountLedgerObject->data(), accountLedgerObject->size()}, + accountKeylet.key}; + if (!accountKeylet.check(sle)) + return Error{RPC::Status{RPC::RippledError::rpcDB_DESERIALIZATION}}; + // Return SignerList(s) if that is requested. + if (input.signerLists) + { + // We put the SignerList in an array because of an anticipated + // future when we support multiple signer lists on one account. + auto const signersKey = ripple::keylet::signers(*accountID); + // This code will need to be revisited if in the future we + // support multiple SignerLists on one account. + auto const signers = sharedPtrBackend_->fetchLedgerObject( + signersKey.key, lgrInfo.seq, yield); + std::vector signerList; + if (signers) + { + ripple::STLedgerEntry const sleSigners{ + ripple::SerialIter{signers->data(), signers->size()}, + signersKey.key}; + if (!signersKey.check(sleSigners)) + return Error{ + RPC::Status{RPC::RippledError::rpcDB_DESERIALIZATION}}; + + signerList.push_back(sleSigners); + } + return Output( + lgrInfo.seq, ripple::strHex(lgrInfo.hash), sle, signerList); + } + return Output(lgrInfo.seq, ripple::strHex(lgrInfo.hash), sle); +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountInfoHandler::Output const& output) +{ + jv = boost::json::object{ + {JS(account_data), RPC::toJson(output.accountData)}, + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}}; + if (output.signerLists) + { + jv.as_object()[JS(signer_lists)] = boost::json::array(); + for (auto const& signerList : output.signerLists.value()) + jv.as_object()[JS(signer_lists)].as_array().push_back( + RPC::toJson(signerList)); + } +} + +AccountInfoHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + AccountInfoHandler::Input input; + if (jsonObject.contains(JS(ident))) + { + input.ident = jsonObject.at(JS(ident)).as_string().c_str(); + } + if (jsonObject.contains(JS(account))) + { + input.account = jsonObject.at(JS(account)).as_string().c_str(); + } + if (jsonObject.contains(JS(ledger_hash))) + { + input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str(); + } + if (jsonObject.contains(JS(ledger_index))) + { + if (!jsonObject.at(JS(ledger_index)).is_string()) + { + input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64(); + } + else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") + { + input.ledgerIndex = + std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str()); + } + } + if (jsonObject.contains(JS(signer_lists))) + { + input.signerLists = jsonObject.at(JS(signer_lists)).as_bool(); + } + return input; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountInfo.h b/src/rpc/ngHandlers/AccountInfo.h new file mode 100644 index 00000000..a194afaf --- /dev/null +++ b/src/rpc/ngHandlers/AccountInfo.h @@ -0,0 +1,112 @@ +//------------------------------------------------------------------------------ +/* + 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 AccountInfoHandler +{ + std::shared_ptr sharedPtrBackend_; + +public: + struct Output + { + uint32_t ledgerIndex; + std::string ledgerHash; + ripple::STLedgerEntry accountData; + std::optional> signerLists; + // validated should be sent via framework + bool validated = true; + + Output( + uint32_t ledgerId, + std::string ledgerHash, + ripple::STLedgerEntry sle, + std::vector signerLists) + : ledgerIndex(ledgerId) + , ledgerHash(std::move(ledgerHash)) + , accountData(std::move(sle)) + , signerLists(std::move(signerLists)) + { + } + + Output( + uint32_t ledgerId, + std::string ledgerHash, + ripple::STLedgerEntry sle) + : ledgerIndex(ledgerId) + , ledgerHash(std::move(ledgerHash)) + , accountData(std::move(sle)) + { + } + }; + + // "queue" is not available in Reporting mode + // "ident" is deprecated, keep it for now, in line with rippled + struct Input + { + std::optional account; + std::optional ident; + std::optional ledgerHash; + std::optional ledgerIndex; + bool signerLists = false; + }; + + using Result = RPCng::HandlerReturnType; + + AccountInfoHandler( + std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static auto const rpcSpec = RpcSpec{ + {JS(account), validation::AccountValidator}, + {JS(ident), validation::AccountValidator}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(signer_lists), validation::Type{}}}; + return rpcSpec; + } + + Result + process(Input input, boost::asio::yield_context& yield) const; +}; + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountInfoHandler::Output const& output); + +AccountInfoHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountOffers.h b/src/rpc/ngHandlers/AccountOffers.h index ccc3c29d..080ca63e 100644 --- a/src/rpc/ngHandlers/AccountOffers.h +++ b/src/rpc/ngHandlers/AccountOffers.h @@ -74,7 +74,7 @@ public: RpcSpecConstRef spec() const { - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(account), validation::Required{}, validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, diff --git a/src/rpc/ngHandlers/AccountTx.h b/src/rpc/ngHandlers/AccountTx.h index 6dc42470..dd1b4741 100644 --- a/src/rpc/ngHandlers/AccountTx.h +++ b/src/rpc/ngHandlers/AccountTx.h @@ -77,7 +77,7 @@ public: RpcSpecConstRef spec() const { - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(account), validation::Required{}, validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, diff --git a/src/rpc/ngHandlers/BookOffers.h b/src/rpc/ngHandlers/BookOffers.h index 958c8d92..8835742d 100644 --- a/src/rpc/ngHandlers/BookOffers.h +++ b/src/rpc/ngHandlers/BookOffers.h @@ -65,7 +65,7 @@ public: RpcSpecConstRef spec() const { - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(taker_gets), validation::Required{}, validation::Type{}, diff --git a/src/rpc/ngHandlers/GatewayBalances.h b/src/rpc/ngHandlers/GatewayBalances.h index f564cc00..bb2e92fc 100644 --- a/src/rpc/ngHandlers/GatewayBalances.h +++ b/src/rpc/ngHandlers/GatewayBalances.h @@ -104,7 +104,7 @@ public: return MaybeError{}; }}; - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(account), validation::Required{}, validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, diff --git a/src/rpc/ngHandlers/LedgerEntry.h b/src/rpc/ngHandlers/LedgerEntry.h index 6ffb0526..216d718b 100644 --- a/src/rpc/ngHandlers/LedgerEntry.h +++ b/src/rpc/ngHandlers/LedgerEntry.h @@ -102,7 +102,7 @@ public: return MaybeError{}; }}; - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(binary), validation::Type{}}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, diff --git a/src/rpc/ngHandlers/TransactionEntry.h b/src/rpc/ngHandlers/TransactionEntry.h index 2c980402..4fb59785 100644 --- a/src/rpc/ngHandlers/TransactionEntry.h +++ b/src/rpc/ngHandlers/TransactionEntry.h @@ -61,7 +61,7 @@ public: RpcSpecConstRef spec() const { - static const RpcSpec rpcSpec = { + static auto const rpcSpec = RpcSpec{ {JS(tx_hash), validation::Required{}, validation::Uint256HexStringValidator}, diff --git a/unittests/rpc/handlers/AccountInfoTest.cpp b/unittests/rpc/handlers/AccountInfoTest.cpp new file mode 100644 index 00000000..37958b2a --- /dev/null +++ b/unittests/rpc/handlers/AccountInfoTest.cpp @@ -0,0 +1,441 @@ +//------------------------------------------------------------------------------ +/* + 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 ACCOUNT1 = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto INDEX1 = + "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; + +class RPCAccountInfoHandlerTest : public HandlerBaseTest +{ +}; + +struct AccountInfoParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct AccountInfoParameterTest + : public RPCAccountInfoHandlerTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = + static_cast(info.param); + return bundle.testName; + } + }; +}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + AccountInfoParamTestCaseBundle{ + "MissingAccountAndIdent", + R"({})", + "actMalformed", + "Account malformed."}, + AccountInfoParamTestCaseBundle{ + "AccountNotString", + R"({"account":1})", + "invalidParams", + "accountNotString"}, + AccountInfoParamTestCaseBundle{ + "AccountInvalid", + R"({"account":"xxx"})", + "invalidParams", + "accountMalformed"}, + AccountInfoParamTestCaseBundle{ + "IdentNotString", + R"({"ident":1})", + "invalidParams", + "identNotString"}, + AccountInfoParamTestCaseBundle{ + "IdentInvalid", + R"({"ident":"xxx"})", + "invalidParams", + "identMalformed"}, + AccountInfoParamTestCaseBundle{ + "SignerListsInvalid", + R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "signer_lists":1})", + "invalidParams", + "Invalid parameters."}, + AccountInfoParamTestCaseBundle{ + "LedgerHashInvalid", + R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":"1"})", + "invalidParams", + "ledger_hashMalformed"}, + AccountInfoParamTestCaseBundle{ + "LedgerHashNotString", + R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_hash":1})", + "invalidParams", + "ledger_hashNotString"}, + AccountInfoParamTestCaseBundle{ + "LedgerIndexInvalid", + R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_index":"a"})", + "invalidParams", + "ledgerIndexMalformed"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCAccountInfoGroup1, + AccountInfoParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + AccountInfoParameterTest::NameGenerator{}); + +TEST_P(AccountInfoParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); + EXPECT_EQ( + err.at("error_message").as_string(), + testBundle.expectedErrorMessage); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, LedgerNonExistViaIntSequence) +{ + 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":"{}", + "ledger_index":30 + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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"); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, LedgerNonExistViaStringSequence) +{ + 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::nullopt)); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "ledger_index":"30" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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"); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, 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{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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"); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, 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{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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"); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, AccountInvalid) +{ + 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)); + // return a valid ledger object but not account root + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0))); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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(), "dbDeserialization"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Database deserialization error."); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, SignerListsInvalid) +{ + 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)); + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + auto const accountRoot = + CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + auto signersKey = ripple::keylet::signers(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(signersKey, 30, _)) + .WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0))); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "signer_lists":true + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& 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(), "dbDeserialization"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Database deserialization error."); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, SignerListsTrue) +{ + auto const expectedOutput = fmt::format( + R"({{ + "account_data":{{ + "Account":"{}", + "Balance":"200", + "Flags":0, + "LedgerEntryType":"AccountRoot", + "OwnerCount":2, + "PreviousTxnID":"{}", + "PreviousTxnLgrSeq":2, + "Sequence":2, + "TransferRate":0, + "index":"13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8" + }}, + "ledger_hash":"{}", + "ledger_index":30, + "signer_lists": + [ + {{ + "Flags":0, + "LedgerEntryType":"SignerList", + "OwnerNode":"0", + "PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq":0, + "SignerEntries": + [ + {{ + "SignerEntry": + {{ + "Account":"{}", + "SignerWeight":1 + }} + }}, + {{ + "SignerEntry": + {{ + "Account":"{}", + "SignerWeight":1 + }} + }} + ], + "SignerListID":0, + "SignerQuorum":2, + "index":"A9C28A28B85CD533217F5C0A0C7767666B093FA58A0F2D80026FCC4CD932DDC7" + }} + ] + }})", + ACCOUNT, + INDEX1, + LEDGERHASH, + ACCOUNT1, + ACCOUNT2); + 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)); + + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + auto const accountRoot = + CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + auto signersKey = ripple::keylet::signers(account).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(signersKey, 30, _)) + .WillByDefault(Return(CreateSignerLists({{ACCOUNT1, 1}, {ACCOUNT2, 1}}) + .getSerializer() + .peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account":"{}", + "signer_lists":true + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, IdentAndSignerListsFalse) +{ + 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)); + + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + auto const accountRoot = + CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "ident":"{}" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_FALSE(output->as_object().contains("signer_lists")); + }); +} diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index d1601f8e..455532c7 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -503,3 +503,29 @@ CreateNFTSellOffer(std::string_view tokenID, std::string_view account) offer.setFieldU64(ripple::sfNFTokenOfferNode, 0ul); return offer; } + +ripple::STObject +CreateSignerLists(std::vector> const& signers) +{ + auto signerlists = ripple::STObject(ripple::sfLedgerEntry); + signerlists.setFieldU16(ripple::sfLedgerEntryType, ripple::ltSIGNER_LIST); + signerlists.setFieldU32(ripple::sfFlags, 0); + signerlists.setFieldU64(ripple::sfOwnerNode, 0); + signerlists.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256()); + signerlists.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + signerlists.setFieldU32(ripple::sfSignerListID, 0); + uint32_t quorum = 0; + ripple::STArray list; + for (auto const& signer : signers) + { + auto entry = ripple::STObject(ripple::sfSignerEntry); + entry.setAccountID( + ripple::sfAccount, GetAccountIDWithString(signer.first)); + entry.setFieldU16(ripple::sfSignerWeight, signer.second); + quorum += signer.second; + list.push_back(entry); + } + signerlists.setFieldU32(ripple::sfSignerQuorum, quorum); + signerlists.setFieldArray(ripple::sfSignerEntries, list); + return signerlists; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h index 31dface3..6c7661e3 100644 --- a/unittests/util/TestObject.h +++ b/unittests/util/TestObject.h @@ -219,3 +219,6 @@ CreateNFTBuyOffer(std::string_view tokenID, std::string_view account); [[nodiscard]] ripple::STObject CreateNFTSellOffer(std::string_view tokenID, std::string_view account); + +ripple::STObject +CreateSignerLists(std::vector> const& signers);