diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a844b7b..772e90ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/AccountCurrencies.cpp src/rpc/ngHandlers/Tx.cpp src/rpc/ngHandlers/GatewayBalances.cpp + src/rpc/ngHandlers/LedgerEntry.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -124,7 +125,8 @@ if(BUILD_TESTS) unittests/rpc/handlers/PingTest.cpp unittests/rpc/handlers/AccountChannelsTest.cpp unittests/rpc/handlers/TxTest.cpp - unittests/rpc/handlers/GatewayBalancesTest.cpp) + unittests/rpc/handlers/GatewayBalancesTest.cpp + unittests/rpc/handlers/LedgerEntryTest.cpp) include(CMake/deps/gtest.cmake) # if CODE_COVERAGE enable, add clio_test-ccov diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index ed665d00..55a131ef 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -36,6 +36,9 @@ Section::verify(boost::json::value const& value, std::string_view key) const // instead auto const& res = value.at(key.data()); + // if it is not a json object, let other validators fail + if (!res.is_object()) + return {}; for (auto const& spec : specs) { if (auto const ret = spec.validate(res); not ret) @@ -97,17 +100,19 @@ checkIsU32Numeric(std::string_view sv) return ec == std::errc(); } -CustomValidator LedgerHashValidator = CustomValidator{ +CustomValidator Uint256HexStringValidator = CustomValidator{ [](boost::json::value const& value, std::string_view key) -> MaybeError { if (!value.is_string()) { return Error{RPC::Status{ - RPC::RippledError::rpcINVALID_PARAMS, "ledgerHashNotString"}}; + RPC::RippledError::rpcINVALID_PARAMS, + std::string(key) + "NotString"}}; } ripple::uint256 ledgerHash; if (!ledgerHash.parseHex(value.as_string().c_str())) return Error{RPC::Status{ - RPC::RippledError::rpcINVALID_PARAMS, "ledgerHashMalformed"}}; + RPC::RippledError::rpcINVALID_PARAMS, + std::string(key) + "Malformed"}}; return MaybeError{}; }}; @@ -146,6 +151,21 @@ CustomValidator AccountValidator = CustomValidator{ return MaybeError{}; }}; +CustomValidator AccountBase58Validator = 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"}}; + } + auto const account = + ripple::parseBase58(value.as_string().c_str()); + if (!account || account->isZero()) + return Error{RPC::Status{RPC::ClioError::rpcMALFORMED_ADDRESS}}; + return MaybeError{}; + }}; + CustomValidator MarkerValidator = CustomValidator{ [](boost::json::value const& value, std::string_view key) -> MaybeError { if (!value.is_string()) @@ -165,7 +185,7 @@ CustomValidator MarkerValidator = CustomValidator{ return MaybeError{}; }}; -CustomValidator TxHashValidator = CustomValidator{ +CustomValidator CurrencyValidator = CustomValidator{ [](boost::json::value const& value, std::string_view key) -> MaybeError { if (!value.is_string()) { @@ -173,12 +193,10 @@ CustomValidator TxHashValidator = CustomValidator{ RPC::RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}}; } - ripple::uint256 txHash; - if (!txHash.parseHex(value.as_string().c_str())) - { + ripple::Currency currency; + if (!ripple::to_currency(currency, value.as_string().c_str())) return Error{RPC::Status{ - RPC::RippledError::rpcINVALID_PARAMS, "malformedTransaction"}}; - } + RPC::ClioError::rpcMALFORMED_CURRENCY, "malformedCurrency"}}; return MaybeError{}; }}; diff --git a/src/rpc/common/Validators.h b/src/rpc/common/Validators.h index 779778cc..63888f22 100644 --- a/src/rpc/common/Validators.h +++ b/src/rpc/common/Validators.h @@ -415,19 +415,13 @@ public: [[nodiscard]] bool checkIsU32Numeric(std::string_view sv); -/** - * @brief Provide a common used validator for ledger hash - * LedgerHash must be a string and hex - */ -extern CustomValidator LedgerIndexValidator; - /** * @brief Provide a common used validator for ledger index * LedgerIndex must be a string or int * If the specified LedgerIndex is a string, it's value must be either * "validated" or a valid integer value represented as a string. */ -extern CustomValidator LedgerHashValidator; +extern CustomValidator LedgerIndexValidator; /** * @brief Provide a common used validator for account @@ -435,6 +429,12 @@ extern CustomValidator LedgerHashValidator; */ extern CustomValidator AccountValidator; +/** + * @brief Provide a common used validator for account + * Account must be a string and can convert to base58 + */ +extern CustomValidator AccountBase58Validator; + /** * @brief Provide a common used validator for marker * Marker is composed of a comma separated index and start hint. The @@ -443,9 +443,16 @@ extern CustomValidator AccountValidator; extern CustomValidator MarkerValidator; /** - * @brief Provide a common used validator for transaction hash + * @brief Provide a common used validator for uint256 hex string * It must be a string and hex + * Transaction index, ledger hash all use this validator */ -extern CustomValidator TxHashValidator; +extern CustomValidator Uint256HexStringValidator; + +/** + * @brief Provide a common used validator for currency + * including standard currency code and token code + */ +extern CustomValidator CurrencyValidator; } // namespace RPCng::validation diff --git a/src/rpc/ngHandlers/AccountChannels.h b/src/rpc/ngHandlers/AccountChannels.h index 0db34565..4a5ef53a 100644 --- a/src/rpc/ngHandlers/AccountChannels.h +++ b/src/rpc/ngHandlers/AccountChannels.h @@ -88,7 +88,7 @@ public: static const RpcSpec rpcSpec = { {"account", validation::Required{}, validation::AccountValidator}, {"destination_account", validation::Type{},validation::AccountValidator}, - {"ledger_hash", validation::LedgerHashValidator}, + {"ledger_hash", validation::Uint256HexStringValidator}, {"limit", validation::Type{},validation::Between{10,400}}, {"ledger_index", validation::LedgerIndexValidator}, {"marker", validation::MarkerValidator} diff --git a/src/rpc/ngHandlers/AccountCurrencies.h b/src/rpc/ngHandlers/AccountCurrencies.h index 6e28949f..c3537a6a 100644 --- a/src/rpc/ngHandlers/AccountCurrencies.h +++ b/src/rpc/ngHandlers/AccountCurrencies.h @@ -65,7 +65,7 @@ public: { static const RpcSpec rpcSpec = { {"account", validation::Required{}, validation::AccountValidator}, - {"ledger_hash", validation::LedgerHashValidator}, + {"ledger_hash", validation::Uint256HexStringValidator}, {"ledger_index", validation::LedgerIndexValidator}}; return rpcSpec; } diff --git a/src/rpc/ngHandlers/GatewayBalances.h b/src/rpc/ngHandlers/GatewayBalances.h index 1014f0dd..fa722a09 100644 --- a/src/rpc/ngHandlers/GatewayBalances.h +++ b/src/rpc/ngHandlers/GatewayBalances.h @@ -105,7 +105,7 @@ public: static const RpcSpec rpcSpec = { {"account", validation::Required{}, validation::AccountValidator}, - {"ledger_hash", validation::LedgerHashValidator}, + {"ledger_hash", validation::Uint256HexStringValidator}, {"ledger_index", validation::LedgerIndexValidator}, {"hotwallet", hotWalletValidator}}; return rpcSpec; diff --git a/src/rpc/ngHandlers/LedgerEntry.cpp b/src/rpc/ngHandlers/LedgerEntry.cpp new file mode 100644 index 00000000..ae7acd77 --- /dev/null +++ b/src/rpc/ngHandlers/LedgerEntry.cpp @@ -0,0 +1,282 @@ +//------------------------------------------------------------------------------ +/* + 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 { +LedgerEntryHandler::Result +LedgerEntryHandler::process( + LedgerEntryHandler::Input input, + boost::asio::yield_context& yield) const +{ + ripple::uint256 key; + if (input.index) + { + key = ripple::uint256{std::string_view(*(input.index))}; + } + else if (input.accountRoot) + { + key = ripple::keylet::account( + *ripple::parseBase58(*(input.accountRoot))) + .key; + } + else if (input.directory) + { + auto const keyOrStatus = composeKeyFromDirectory(*input.directory); + if (auto const status = std::get_if(&keyOrStatus)) + return Error{*status}; + key = std::get(keyOrStatus); + } + else if (input.offer) + { + auto const id = ripple::parseBase58( + input.offer->at("account").as_string().c_str()); + key = ripple::keylet::offer( + *id, + boost::json::value_to(input.offer->at("seq"))) + .key; + } + else if (input.rippleStateAccount) + { + auto const id1 = ripple::parseBase58( + input.rippleStateAccount->at("accounts") + .as_array() + .at(0) + .as_string() + .c_str()); + auto const id2 = ripple::parseBase58( + input.rippleStateAccount->at("accounts") + .as_array() + .at(1) + .as_string() + .c_str()); + auto const currency = ripple::to_currency( + input.rippleStateAccount->at("currency").as_string().c_str()); + key = ripple::keylet::line(*id1, *id2, currency).key; + } + else if (input.escrow) + { + auto const id = ripple::parseBase58( + input.escrow->at("owner").as_string().c_str()); + key = + ripple::keylet::escrow(*id, input.escrow->at("seq").as_int64()).key; + } + else if (input.depositPreauth) + { + auto const owner = ripple::parseBase58( + input.depositPreauth->at("owner").as_string().c_str()); + auto const authorized = ripple::parseBase58( + input.depositPreauth->at("authorized").as_string().c_str()); + key = ripple::keylet::depositPreauth(*owner, *authorized).key; + } + else if (input.ticket) + { + auto const id = ripple::parseBase58( + input.ticket->at("account").as_string().c_str()); + key = ripple::getTicketIndex( + *id, input.ticket->at("ticket_seq").as_int64()); + } + else + { + // Must specify 1 of the following fields to indicate what type + return Error{ + RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "unknownOption"}}; + } + + // check ledger exists + 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 ledgerObject = + sharedPtrBackend_->fetchLedgerObject(key, lgrInfo.seq, yield); + if (!ledgerObject || ledgerObject->size() == 0) + return Error{RPC::Status{"entryNotFound"}}; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{ledgerObject->data(), ledgerObject->size()}, key}; + if (input.expectedType != ripple::ltANY && + sle.getType() != input.expectedType) + return Error{RPC::Status{"unexpectedLedgerType"}}; + + LedgerEntryHandler::Output output; + output.index = ripple::strHex(key); + output.ledgerIndex = lgrInfo.seq; + output.ledgerHash = ripple::strHex(lgrInfo.hash); + if (input.binary) + { + output.nodeBinary = ripple::strHex(*ledgerObject); + } + else + { + output.node = RPC::toJson(sle); + } + return output; +} + +std::variant +LedgerEntryHandler::composeKeyFromDirectory( + boost::json::object const& directory) const noexcept +{ + // can not specify both dir_root and owner. + if (directory.contains("dir_root") && directory.contains("owner")) + return RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + "mayNotSpecifyBothDirRootAndOwner"}; + // at least one should availiable + if (!(directory.contains("dir_root") || directory.contains("owner"))) + return RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, "missingOwnerOrDirRoot"}; + + uint64_t const subIndex = directory.contains("sub_index") + ? boost::json::value_to(directory.at("sub_index")) + : 0; + + if (directory.contains("dir_root")) + { + ripple::uint256 const uDirRoot{ + directory.at("dir_root").as_string().c_str()}; + return ripple::keylet::page(uDirRoot, subIndex).key; + } + + auto const ownerID = ripple::parseBase58( + directory.at("owner").as_string().c_str()); + return ripple::keylet::page(ripple::keylet::ownerDir(*ownerID), subIndex) + .key; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + LedgerEntryHandler::Output const& output) +{ + auto object = boost::json::object{ + {"ledger_hash", output.ledgerHash}, + {"ledger_index", output.ledgerIndex}, + {"validated", output.validated}, + {"index", output.index}}; + if (output.nodeBinary) + { + object["node_binary"] = *(output.nodeBinary); + } + else + { + object["node"] = *(output.node); + } + jv = std::move(object); +} + +LedgerEntryHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + LedgerEntryHandler::Input input; + 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()); + } + } + if (jsonObject.contains("binary")) + { + input.binary = jv.at("binary").as_bool(); + } + // check all the protential index + static auto const indexFieldTypeMap = + std::unordered_map{ + {"index", ripple::ltANY}, + {"directory", ripple::ltDIR_NODE}, + {"offer", ripple::ltOFFER}, + {"check", ripple::ltCHECK}, + {"escrow", ripple::ltESCROW}, + {"payment_channel", ripple::ltPAYCHAN}, + {"deposit_preauth", ripple::ltDEPOSIT_PREAUTH}, + {"ticket", ripple::ltTICKET}}; + + auto const indexFieldType = std::find_if( + indexFieldTypeMap.begin(), + indexFieldTypeMap.end(), + [&jsonObject](auto const& pair) { + auto const& [field, _] = pair; + return jsonObject.contains(field) && + jsonObject.at(field).is_string(); + }); + if (indexFieldType != indexFieldTypeMap.end()) + { + input.index = jv.at(indexFieldType->first).as_string().c_str(); + input.expectedType = indexFieldType->second; + } + // check if request for account root + else if (jsonObject.contains("account_root")) + { + input.accountRoot = jv.at("account_root").as_string().c_str(); + } + // no need to check if_object again, validator only allows string or object + else if (jsonObject.contains("directory")) + { + input.directory = jv.at("directory").as_object(); + } + else if (jsonObject.contains("offer")) + { + input.offer = jv.at("offer").as_object(); + } + else if (jsonObject.contains("ripple_state")) + { + input.rippleStateAccount = jv.at("ripple_state").as_object(); + } + else if (jsonObject.contains("escrow")) + { + input.escrow = jv.at("escrow").as_object(); + } + else if (jsonObject.contains("deposit_preauth")) + { + input.depositPreauth = jv.at("deposit_preauth").as_object(); + } + else if (jsonObject.contains("ticket")) + { + input.ticket = jv.at("ticket").as_object(); + } + return input; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/LedgerEntry.h b/src/rpc/ngHandlers/LedgerEntry.h new file mode 100644 index 00000000..2a2f948b --- /dev/null +++ b/src/rpc/ngHandlers/LedgerEntry.h @@ -0,0 +1,209 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { + +class LedgerEntryHandler +{ + std::shared_ptr sharedPtrBackend_; + +public: + struct Output + { + std::string index; + uint32_t ledgerIndex; + std::string ledgerHash; + std::optional node; + std::optional nodeBinary; + bool validated = true; + }; + + // TODO: nft_page has not been implemented + struct Input + { + std::optional ledgerHash; + std::optional ledgerIndex; + bool binary = false; + // id of this ledger entry: 256 bits hex string + std::optional index; + // index can be extracted from payment_channel, check, escrow, offer + // etc, expectedType is used to save the type of index + ripple::LedgerEntryType expectedType = ripple::ltANY; + // account id to address account root object + std::optional accountRoot; + // TODO: extract into custom objects, remove json from Input + std::optional directory; + std::optional offer; + std::optional rippleStateAccount; + std::optional escrow; + std::optional depositPreauth; + std::optional ticket; + }; + + using Result = RPCng::HandlerReturnType; + + LedgerEntryHandler( + std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + // Validator only works in this handler + // The accounts array must have two different elements + // Each element must be a valid address + static auto const rippleStateAccountsCheck = + validation::CustomValidator{ + [](boost::json::value const& value, + std::string_view key) -> MaybeError { + if (!value.is_array() || value.as_array().size() != 2 || + !value.as_array()[0].is_string() || + !value.as_array()[1].is_string() || + value.as_array()[0].as_string() == + value.as_array()[1].as_string()) + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + "malformedAccounts"}}; + auto const id1 = ripple::parseBase58( + value.as_array()[0].as_string().c_str()); + auto const id2 = ripple::parseBase58( + value.as_array()[1].as_string().c_str()); + if (!id1 || !id2) + return Error{RPC::Status{ + RPC::ClioError::rpcMALFORMED_ADDRESS, + "malformedAddresses"}}; + return MaybeError{}; + }}; + + static const RpcSpec rpcSpec = { + {"binary", validation::Type{}}, + {"ledger_hash", validation::Uint256HexStringValidator}, + {"ledger_index", validation::LedgerIndexValidator}, + {"index", validation::Uint256HexStringValidator}, + {"account_root", validation::AccountBase58Validator}, + {"check", validation::Uint256HexStringValidator}, + {"deposit_preauth", + validation::Type{}, + validation::IfType{ + validation::Uint256HexStringValidator}, + validation::IfType{ + validation::Section{ + {"owner", + validation::Required{}, + validation::AccountBase58Validator}, + {"authorized", + validation::Required{}, + validation::AccountBase58Validator}, + }, + }}, + {"directory", + validation::Type{}, + validation::IfType{ + validation::Uint256HexStringValidator}, + validation::IfType{validation::Section{ + {"owner", validation::AccountBase58Validator}, + {"dir_root", validation::Uint256HexStringValidator}, + {"sub_index", validation::Type{}}}}}, + {"escrow", + validation::Type{}, + validation::IfType{ + validation::Uint256HexStringValidator}, + validation::IfType{ + validation::Section{ + {"owner", + validation::Required{}, + validation::AccountBase58Validator}, + {"seq", + validation::Required{}, + validation::Type{}}, + }, + }}, + {"offer", + validation::Type{}, + validation::IfType{ + validation::Uint256HexStringValidator}, + validation::IfType{ + validation::Section{ + {"account", + validation::Required{}, + validation::AccountBase58Validator}, + {"seq", + validation::Required{}, + validation::Type{}}, + }, + }}, + {"payment_channel", validation::Uint256HexStringValidator}, + {"ripple_state", + validation::Type{}, + validation::Section{ + {"accounts", validation::Required{}, rippleStateAccountsCheck}, + {"currency", + validation::Required{}, + validation::CurrencyValidator}, + }}, + {"ticket", + validation::Type{}, + validation::IfType{ + validation::Uint256HexStringValidator}, + validation::IfType{ + validation::Section{ + {"account", + validation::Required{}, + validation::AccountBase58Validator}, + {"ticket_seq", + validation::Required{}, + validation::Type{}}, + }, + }}, + }; + return rpcSpec; + } + + Result + process(Input input, boost::asio::yield_context& yield) const; + +private: + // dir_root and owner can not be both empty or filled at the same time + // This function will return an error if this is the case + std::variant + composeKeyFromDirectory( + boost::json::object const& directory) const noexcept; +}; + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + LedgerEntryHandler::Output const& output); + +LedgerEntryHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); +} // namespace RPCng diff --git a/src/rpc/ngHandlers/Tx.h b/src/rpc/ngHandlers/Tx.h index 9385da23..06690b0b 100644 --- a/src/rpc/ngHandlers/Tx.h +++ b/src/rpc/ngHandlers/Tx.h @@ -65,7 +65,7 @@ public: static const RpcSpec rpcSpec = { {"transaction", validation::Required{}, - validation::TxHashValidator}, + validation::Uint256HexStringValidator}, {"binary", validation::Type{}}, {"min_ledger", validation::Type{}}, {"max_ledger", validation::Type{}}, diff --git a/unittests/rpc/BaseTests.cpp b/unittests/rpc/BaseTests.cpp index 0f5a4f07..3693f5c0 100644 --- a/unittests/rpc/BaseTests.cpp +++ b/unittests/rpc/BaseTests.cpp @@ -230,7 +230,7 @@ TEST_F(RPCBaseTest, IfTypeValidator) Section{{ "limit", Required{}, Type{}, Between{0, 100}}}, Section{{ "limit2", Required{}, Type{}, Between{0, 100}}} }, - IfType{LedgerHashValidator,} + IfType{Uint256HexStringValidator,} }}; // clang-format on // if json object pass @@ -281,24 +281,6 @@ TEST_F(RPCBaseTest, CustomValidator) ASSERT_FALSE(spec.validate(failingInput)); } -TEST_F(RPCBaseTest, LedgerHashValidator) -{ - auto spec = RpcSpec{ - {"ledgerHash", LedgerHashValidator}, - }; - auto passingInput = json::parse( - R"({ "ledgerHash": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" })"); - ASSERT_TRUE(spec.validate(passingInput)); - - auto failingInput = json::parse(R"({ "ledgerHash": "wrongformat" })"); - ASSERT_FALSE(spec.validate(failingInput)); - - failingInput = json::parse(R"({ "ledgerHash": 256 })"); - auto err = spec.validate(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "ledgerHashNotString"); -} - TEST_F(RPCBaseTest, LedgerIndexValidator) { auto spec = RpcSpec{ @@ -362,9 +344,9 @@ TEST_F(RPCBaseTest, MarkerValidator) ASSERT_TRUE(spec.validate(passingInput)); } -TEST_F(RPCBaseTest, TxHashValidator) +TEST_F(RPCBaseTest, Uint256HexStringValidator) { - auto const spec = RpcSpec{{"transaction", TxHashValidator}}; + auto const spec = RpcSpec{{"transaction", Uint256HexStringValidator}}; auto const passingInput = json::parse( R"({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"})"); ASSERT_TRUE(spec.validate(passingInput)); @@ -378,5 +360,26 @@ TEST_F(RPCBaseTest, TxHashValidator) R"({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC"})"); err = spec.validate(failingInput); ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "malformedTransaction"); + ASSERT_EQ(err.error().message, "transactionMalformed"); +} + +TEST_F(RPCBaseTest, CurrencyValidator) +{ + auto const spec = RpcSpec{{"currency", CurrencyValidator}}; + auto passingInput = json::parse(R"({ "currency": "GBP"})"); + ASSERT_TRUE(spec.validate(passingInput)); + + passingInput = json::parse( + R"({ "currency": "0158415500000000C1F76FF6ECB0BAC600000000"})"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto failingInput = json::parse(R"({ "currency": 256})"); + auto err = spec.validate(failingInput); + ASSERT_FALSE(err); + ASSERT_EQ(err.error().message, "currencyNotString"); + + failingInput = json::parse(R"({ "currency": "12314"})"); + err = spec.validate(failingInput); + ASSERT_FALSE(err); + ASSERT_EQ(err.error().message, "malformedCurrency"); } diff --git a/unittests/rpc/handlers/AccountChannelsTest.cpp b/unittests/rpc/handlers/AccountChannelsTest.cpp index 96006fe5..7b3b4689 100644 --- a/unittests/rpc/handlers/AccountChannelsTest.cpp +++ b/unittests/rpc/handlers/AccountChannelsTest.cpp @@ -60,7 +60,7 @@ TEST_F(RPCAccountHandlerTest, NonHexLedgerHash) auto const err = RPC::makeError(output.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerHashMalformed"); + EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashMalformed"); }); ctx.run(); } @@ -81,7 +81,7 @@ TEST_F(RPCAccountHandlerTest, NonStringLedgerHash) auto const err = RPC::makeError(output.error()); EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerHashNotString"); + EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashNotString"); }); ctx.run(); } diff --git a/unittests/rpc/handlers/GatewayBalancesTest.cpp b/unittests/rpc/handlers/GatewayBalancesTest.cpp index 796913ec..ac7b5d11 100644 --- a/unittests/rpc/handlers/GatewayBalancesTest.cpp +++ b/unittests/rpc/handlers/GatewayBalancesTest.cpp @@ -127,7 +127,7 @@ generateParameterTestBundles() }})", ACCOUNT), "invalidParams", - "ledgerHashMalformed"}, + "ledger_hashMalformed"}, ParameterTestBundle{ "LedgerHashNotString", fmt::format( @@ -137,7 +137,7 @@ generateParameterTestBundles() }})", ACCOUNT), "invalidParams", - "ledgerHashNotString"}, + "ledger_hashNotString"}, ParameterTestBundle{ "WalletsNotStringOrArray", fmt::format( diff --git a/unittests/rpc/handlers/LedgerEntryTest.cpp b/unittests/rpc/handlers/LedgerEntryTest.cpp new file mode 100644 index 00000000..01fca9a7 --- /dev/null +++ b/unittests/rpc/handlers/LedgerEntryTest.cpp @@ -0,0 +1,1041 @@ +//------------------------------------------------------------------------------ +/* + 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 INDEX1 = + "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto RANGEMIN = 10; +constexpr static auto RANGEMAX = 30; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; + +class RPCLedgerEntryTest : public HandlerBaseTest +{ +}; + +struct ParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct LedgerEntryParameterTest : public RPCLedgerEntryTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = static_cast(info.param); + return bundle.testName; + } + }; +}; + +// TODO: because we extract the error generation from the handler to framework +// the error messages need one round fine tuning +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + ParamTestCaseBundle{ + "InvalidBinaryType", + R"({ + "index": + "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD", + "binary": "invalid" + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidAccountRootFormat", + R"({ + "account_root": "invalid" + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidAccountRootNotString", + R"({ + "account_root": 123 + })", + "invalidParams", + "account_rootNotString"}, + + ParamTestCaseBundle{ + "UnknownOption", + R"({ + })", + "invalidParams", + "unknownOption"}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthType", + R"({ + "deposit_preauth": 123 + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthString", + R"({ + "deposit_preauth": "invalid" + })", + "invalidParams", + "deposit_preauthMalformed"}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthEmtpyJson", + R"({ + "deposit_preauth": { + } + })", + "invalidParams", + "Required field 'owner' missing"}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthJsonWrongAccount", + R"({ + "deposit_preauth": { + "owner": "invalid", + "authorized": "invalid" + } + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthJsonOwnerNotString", + R"({ + "deposit_preauth": { + "owner": 123, + "authorized": 123 + } + })", + "invalidParams", + "ownerNotString"}, + + ParamTestCaseBundle{ + "InvalidDepositPreauthJsonAuthorizedNotString", + fmt::format( + R"({{ + "deposit_preauth": {{ + "owner": "{}", + "authorized": 123 + }} + }})", + ACCOUNT), + "invalidParams", + "authorizedNotString"}, + + ParamTestCaseBundle{ + "InvalidTicketType", + R"({ + "ticket": 123 + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidTicketIndex", + R"({ + "ticket": "invalid" + })", + "invalidParams", + "ticketMalformed"}, + + ParamTestCaseBundle{ + "InvalidTicketEmptyJson", + R"({ + "ticket": {} + })", + "invalidParams", + "Required field 'account' missing"}, + + ParamTestCaseBundle{ + "InvalidTicketJsonAccountNotString", + R"({ + "ticket": { + "account": 123, + "ticket_seq": 123 + } + })", + "invalidParams", + "accountNotString"}, + + ParamTestCaseBundle{ + "InvalidTicketJsonAccountInvalid", + R"({ + "ticket": { + "account": "123", + "ticket_seq": 123 + } + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidTicketJsonSeqNotInt", + fmt::format( + R"({{ + "ticket": {{ + "account": "{}", + "ticket_seq": "123" + }} + }})", + ACCOUNT), + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidOfferType", + R"({ + "offer": 123 + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidOfferIndex", + R"({ + "offer": "invalid" + })", + "invalidParams", + "offerMalformed"}, + + ParamTestCaseBundle{ + "InvalidOfferEmptyJson", + R"({ + "offer": {} + })", + "invalidParams", + "Required field 'account' missing"}, + + ParamTestCaseBundle{ + "InvalidOfferJsonAccountNotString", + R"({ + "ticket": { + "account": 123, + "seq": 123 + } + })", + "invalidParams", + "accountNotString"}, + + ParamTestCaseBundle{ + "InvalidOfferJsonAccountInvalid", + R"({ + "ticket": { + "account": "123", + "seq": 123 + } + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidOfferJsonSeqNotInt", + fmt::format( + R"({{ + "offer": {{ + "account": "{}", + "seq": "123" + }} + }})", + ACCOUNT), + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidEscrowType", + R"({ + "escrow": 123 + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidEscrowIndex", + R"({ + "escrow": "invalid" + })", + "invalidParams", + "escrowMalformed"}, + + ParamTestCaseBundle{ + "InvalidEscrowEmptyJson", + R"({ + "escrow": {} + })", + "invalidParams", + "Required field 'owner' missing"}, + + ParamTestCaseBundle{ + "InvalidEscrowJsonAccountNotString", + R"({ + "escrow": { + "owner": 123, + "seq": 123 + } + })", + "invalidParams", + "ownerNotString"}, + + ParamTestCaseBundle{ + "InvalidEscrowJsonAccountInvalid", + R"({ + "ticket": { + "account": "123", + "seq": 123 + } + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidEscrowJsonSeqNotInt", + fmt::format( + R"({{ + "escrow": {{ + "owner": "{}", + "seq": "123" + }} + }})", + ACCOUNT), + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidRippleStateType", + R"({ + "ripple_state": "123" + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidRippleStateMissField", + R"({ + "ripple_state": { + "currency": "USD" + } + })", + "invalidParams", + "Required field 'accounts' missing"}, + + ParamTestCaseBundle{ + "InvalidRippleStateEmtpyJson", + R"({ + "ripple_state": { + } + })", + "invalidParams", + "Required field 'accounts' missing"}, + + ParamTestCaseBundle{ + "InvalidRippleStateOneAccount", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}"] + }} + }})", + ACCOUNT), + "invalidParams", + "malformedAccounts"}, + + ParamTestCaseBundle{ + "InvalidRippleStateSameAccounts", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}","{}"], + "currency": "USD" + }} + }})", + ACCOUNT, + ACCOUNT), + "invalidParams", + "malformedAccounts"}, + + ParamTestCaseBundle{ + "InvalidRippleStateWrongAccountsNotString", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}",123], + "currency": "USD" + }} + }})", + ACCOUNT), + "invalidParams", + "malformedAccounts"}, + + ParamTestCaseBundle{ + "InvalidRippleStateWrongAccountsFormat", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}","123"], + "currency": "USD" + }} + }})", + ACCOUNT), + "malformedAddress", + "malformedAddresses"}, + + ParamTestCaseBundle{ + "InvalidRippleStateWrongCurrency", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}","{}"], + "currency": "XXXX" + }} + }})", + ACCOUNT, + ACCOUNT2), + "malformedCurrency", + "malformedCurrency"}, + + ParamTestCaseBundle{ + "InvalidRippleStateWrongCurrencyNotString", + fmt::format( + R"({{ + "ripple_state": {{ + "accounts" : ["{}","{}"], + "currency": 123 + }} + }})", + ACCOUNT, + ACCOUNT2), + "invalidParams", + "currencyNotString"}, + + ParamTestCaseBundle{ + "InvalidDirectoryType", + R"({ + "directory": 123 + })", + "invalidParams", + "Invalid parameters."}, + + ParamTestCaseBundle{ + "InvalidDirectoryIndex", + R"({ + "directory": "123" + })", + "invalidParams", + "directoryMalformed"}, + + ParamTestCaseBundle{ + "InvalidDirectoryEmtpyJson", + R"({ + "directory": {} + })", + "invalidParams", + "missingOwnerOrDirRoot"}, + + ParamTestCaseBundle{ + "InvalidDirectoryWrongOwnerNotString", + R"({ + "directory": { + "owner": 123 + } + })", + "invalidParams", + "ownerNotString"}, + + ParamTestCaseBundle{ + "InvalidDirectoryWrongOwnerFormat", + R"({ + "directory": { + "owner": "123" + } + })", + "malformedAddress", + "Malformed address."}, + + ParamTestCaseBundle{ + "InvalidDirectoryWrongDirFormat", + R"({ + "directory": { + "dir_root": "123" + } + })", + "invalidParams", + "dir_rootMalformed"}, + + ParamTestCaseBundle{ + "InvalidDirectoryWrongDirNotString", + R"({ + "directory": { + "dir_root": 123 + } + })", + "invalidParams", + "dir_rootNotString"}, + + ParamTestCaseBundle{ + "InvalidDirectoryDirOwnerConflict", + fmt::format( + R"({{ + "directory": {{ + "dir_root": "{}", + "owner": "{}" + }} + }})", + INDEX1, + ACCOUNT), + "invalidParams", + "mayNotSpecifyBothDirRootAndOwner"}, + + ParamTestCaseBundle{ + "InvalidDirectoryDirSubIndexNotInt", + fmt::format( + R"({{ + "directory": {{ + "dir_root": "{}", + "sub_index": "not int" + }} + }})", + INDEX1), + "invalidParams", + "Invalid parameters."}}; +} + +INSTANTIATE_TEST_CASE_P( + RPCLedgerEntryGroup1, + LedgerEntryParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + LedgerEntryParameterTest::NameGenerator{}); + +TEST_P(LedgerEntryParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{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); + std::cout << err.at("error").as_string() << std::endl; + std::cout << err.at("error_message").as_string() << std::endl; + EXPECT_EQ( + err.at("error_message").as_string(), + testBundle.expectedErrorMessage); + }); + ctx.run(); +} + +// parameterized test cases for index +struct IndexTest : public HandlerBaseTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + return static_cast(info.param); + } + }; +}; + +// content of index, payment_channel, check fields is ledger index +INSTANTIATE_TEST_CASE_P( + RPCLedgerEntryGroup3, + IndexTest, + Values("index", "payment_channel", "check"), + IndexTest::NameGenerator{}); + +TEST_P(IndexTest, InvalidIndexUint256) +{ + auto const index = GetParam(); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "{}": "invalid" + }})", + index)); + auto const output = handler.process(req, 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(), index + "Malformed"); + }); + ctx.run(); +} + +TEST_P(IndexTest, InvalidIndexNotString) +{ + auto const index = GetParam(); + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "{}": 123 + }})", + index)); + auto const output = handler.process(req, 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(), index + "NotString"); + }); + ctx.run(); +} + +TEST_F(RPCLedgerEntryTest, LedgerEntryNotFound) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(ledgerinfo)); + + // return null for ledger entry + auto const key = + ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(key, RANGEMAX, _)) + .WillByDefault(Return(std::optional{})); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "account_root": "{}" + }})", + ACCOUNT)); + auto const output = handler.process(req, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "entryNotFound"); + }); + ctx.run(); +} + +struct NormalPathTestBundle +{ + std::string testName; + std::string testJson; + ripple::uint256 expectedIndex; + ripple::STObject mockedEntity; +}; + +struct RPCLedgerEntryNormalPathTest + : public RPCLedgerEntryTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = static_cast(info.param); + return bundle.testName; + } + }; +}; + +static auto +generateTestValuesForNormalPathTest() +{ + auto account1 = GetAccountIDWithString(ACCOUNT); + auto account2 = GetAccountIDWithString(ACCOUNT2); + ripple::Currency currency; + ripple::to_currency(currency, "USD"); + + return std::vector{ + NormalPathTestBundle{ + "Index", + fmt::format( + R"({{ + "binary": true, + "index": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateAccountRootObject( + ACCOUNT2, ripple::lsfGlobalFreeze, 1, 10, 2, INDEX1, 3)}, + NormalPathTestBundle{ + "Payment_channel", + fmt::format( + R"({{ + "binary": true, + "payment_channel": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400)}, + NormalPathTestBundle{ + "Check", + fmt::format( + R"({{ + "binary": true, + "check": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateCheckLedgerObject(ACCOUNT, ACCOUNT2)}, + NormalPathTestBundle{ + "DirectoryIndex", + fmt::format( + R"({{ + "binary": true, + "directory": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateOwnerDirLedgerObject( + std::vector{ripple::uint256{INDEX1}}, INDEX1)}, + NormalPathTestBundle{ + "OfferIndex", + fmt::format( + R"({{ + "binary": true, + "offer": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateOfferLedgerObject(ACCOUNT, 100, 200, "USD", ACCOUNT2)}, + NormalPathTestBundle{ + "EscrowIndex", + fmt::format( + R"({{ + "binary": true, + "escrow": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateEscrowLedgerObject(ACCOUNT, ACCOUNT2)}, + NormalPathTestBundle{ + "TicketIndex", + fmt::format( + R"({{ + "binary": true, + "ticket": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateTicketLedgerObject(ACCOUNT, 0)}, + NormalPathTestBundle{ + "DepositPreauthIndex", + fmt::format( + R"({{ + "binary": true, + "deposit_preauth": "{}" + }})", + INDEX1), + ripple::uint256{INDEX1}, + CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2)}, + NormalPathTestBundle{ + "AccountRoot", + fmt::format( + R"({{ + "binary": true, + "account_root": "{}" + }})", + ACCOUNT), + ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, + CreateAccountRootObject(ACCOUNT, 0, 1, 1, 1, INDEX1, 1)}, + NormalPathTestBundle{ + "DirectoryViaDirRoot", + fmt::format( + R"({{ + "binary": true, + "directory": {{ + "dir_root": "{}", + "sub_index": 2 + }} + }})", + INDEX1), + ripple::keylet::page(ripple::uint256{INDEX1}, 2).key, + CreateOwnerDirLedgerObject( + std::vector{ripple::uint256{INDEX1}}, INDEX1)}, + NormalPathTestBundle{ + "DirectoryViaOwner", + fmt::format( + R"({{ + "binary": true, + "directory": {{ + "owner": "{}", + "sub_index": 2 + }} + }})", + ACCOUNT), + ripple::keylet::page(ripple::keylet::ownerDir(account1), 2).key, + CreateOwnerDirLedgerObject( + std::vector{ripple::uint256{INDEX1}}, INDEX1)}, + NormalPathTestBundle{ + "DirectoryViaDefaultSubIndex", + fmt::format( + R"({{ + "binary": true, + "directory": {{ + "owner": "{}" + }} + }})", + ACCOUNT), + // default sub_index is 0 + ripple::keylet::page(ripple::keylet::ownerDir(account1), 0).key, + CreateOwnerDirLedgerObject( + std::vector{ripple::uint256{INDEX1}}, INDEX1)}, + NormalPathTestBundle{ + "Escrow", + fmt::format( + R"({{ + "binary": true, + "escrow": {{ + "owner": "{}", + "seq": 1 + }} + }})", + ACCOUNT), + ripple::keylet::escrow(account1, 1).key, + CreateEscrowLedgerObject(ACCOUNT, ACCOUNT2)}, + NormalPathTestBundle{ + "DepositPreauth", + fmt::format( + R"({{ + "binary": true, + "deposit_preauth": {{ + "owner": "{}", + "authorized": "{}" + }} + }})", + ACCOUNT, + ACCOUNT2), + ripple::keylet::depositPreauth(account1, account2).key, + CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2)}, + NormalPathTestBundle{ + "RippleState", + fmt::format( + R"({{ + "binary": true, + "ripple_state": {{ + "accounts": ["{}","{}"], + "currency": "USD" + }} + }})", + ACCOUNT, + ACCOUNT2), + ripple::keylet::line(account1, account2, currency).key, + CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ACCOUNT2, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + INDEX1, + 123)}, + NormalPathTestBundle{ + "Ticket", + fmt::format( + R"({{ + "binary": true, + "ticket": {{ + "account": "{}", + "ticket_seq": 2 + }} + }})", + ACCOUNT), + ripple::getTicketIndex(account1, 2), + CreateTicketLedgerObject(ACCOUNT, 0)}, + NormalPathTestBundle{ + "Offer", + fmt::format( + R"({{ + "binary": true, + "offer": {{ + "account": "{}", + "seq": 2 + }} + }})", + ACCOUNT), + ripple::keylet::offer(account1, 2).key, + CreateOfferLedgerObject(ACCOUNT, 100, 200, "USD", ACCOUNT2)}}; +} + +INSTANTIATE_TEST_CASE_P( + RPCLedgerEntryGroup2, + RPCLedgerEntryNormalPathTest, + ValuesIn(generateTestValuesForNormalPathTest()), + RPCLedgerEntryNormalPathTest::NameGenerator{}); + +// Test for normal path +// Check the index in response matches the computed index accordingly +TEST_P(RPCLedgerEntryNormalPathTest, NormalPath) +{ + auto const testBundle = GetParam(); + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(ledgerinfo)); + + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(testBundle.expectedIndex, RANGEMAX, _)) + .WillByDefault( + Return(testBundle.mockedEntity.getSerializer().peekData())); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, yield); + ASSERT_TRUE(output); + EXPECT_EQ(output.value().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output.value().at("ledger_index").as_uint64(), RANGEMAX); + EXPECT_EQ( + output.value().at("node_binary").as_string(), + ripple::strHex(testBundle.mockedEntity.getSerializer().peekData())); + EXPECT_EQ( + ripple::uint256(output.value().at("index").as_string().c_str()), + testBundle.expectedIndex); + }); + ctx.run(); +} + +// this testcase will test the deserialization of ledger entry +TEST_F(RPCLedgerEntryTest, BinaryFalse) +{ + static auto constexpr OUT = R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "validated":true, + "index":"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD", + "node":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"100", + "Balance":"200", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Flags":0, + "LedgerEntryType":"PayChannel", + "OwnerNode":"0", + "PreviousTxnID":"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD", + "PreviousTxnLgrSeq":400, + "PublicKey":"020000000000000000000000000000000000000000000000000000000000000000", + "SettleDelay":300, + "index":"05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD" + } + })"; + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(ledgerinfo)); + + // return valid ledger entry which can be deserialized + auto const ledgerEntry = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillByDefault(Return(ledgerEntry.getSerializer().peekData())); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "payment_channel": "{}" + }})", + INDEX1)); + auto const output = handler.process(req, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(OUT)); + }); + ctx.run(); +} + +TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, RANGEMAX); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(ledgerinfo)); + + // return valid ledger entry which can be deserialized + auto const ledgerEntry = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 200, 300, INDEX1, 400); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ripple::uint256{INDEX1}, RANGEMAX, _)) + .WillByDefault(Return(ledgerEntry.getSerializer().peekData())); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{LedgerEntryHandler{mockBackendPtr}}; + auto const req = json::parse(fmt::format( + R"({{ + "check": "{}" + }})", + INDEX1)); + auto const output = handler.process(req, yield); + ASSERT_FALSE(output); + }); + ctx.run(); +} diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index 9d4201c0..35d6bb4b 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -321,3 +321,94 @@ CreateRippleStateLedgerObject( line.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxnSeq); return line; } + +ripple::STObject +CreateOfferLedgerObject( + std::string_view account, + int takerGets, + int takerPays, + std::string_view currency, + std::string_view issueId) +{ + ripple::STObject offer(ripple::sfLedgerEntry); + offer.setFieldU16(ripple::sfLedgerEntryType, ripple::ltOFFER); + offer.setAccountID(ripple::sfAccount, GetAccountIDWithString(account)); + offer.setFieldU32(ripple::sfSequence, 0); + offer.setFieldU32(ripple::sfFlags, 0); + ripple::Issue issue1 = GetIssue(currency, issueId); + offer.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(issue1, takerGets)); + offer.setFieldAmount( + ripple::sfTakerPays, ripple::STAmount(takerPays, false)); + offer.setFieldH256(ripple::sfBookDirectory, ripple::uint256{}); + offer.setFieldU64(ripple::sfBookNode, 0); + offer.setFieldU64(ripple::sfOwnerNode, 0); + offer.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + offer.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return offer; +} + +ripple::STObject +CreateTicketLedgerObject(std::string_view account, uint32_t sequence) +{ + ripple::STObject ticket(ripple::sfLedgerEntry); + ticket.setFieldU16(ripple::sfLedgerEntryType, ripple::ltTICKET); + ticket.setAccountID(ripple::sfAccount, GetAccountIDWithString(account)); + ticket.setFieldU32(ripple::sfFlags, 0); + ticket.setFieldU64(ripple::sfOwnerNode, 0); + ticket.setFieldU32(ripple::sfTicketSequence, sequence); + ticket.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + ticket.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return ticket; +} + +ripple::STObject +CreateEscrowLedgerObject(std::string_view account, std::string_view dest) +{ + ripple::STObject escrow(ripple::sfLedgerEntry); + escrow.setFieldU16(ripple::sfLedgerEntryType, ripple::ltESCROW); + escrow.setAccountID(ripple::sfAccount, GetAccountIDWithString(account)); + escrow.setAccountID(ripple::sfDestination, GetAccountIDWithString(dest)); + escrow.setFieldAmount(ripple::sfAmount, ripple::STAmount(0, false)); + escrow.setFieldU64(ripple::sfOwnerNode, 0); + escrow.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + escrow.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + escrow.setFieldU32(ripple::sfFlags, 0); + return escrow; +} + +ripple::STObject +CreateCheckLedgerObject(std::string_view account, std::string_view dest) +{ + ripple::STObject check(ripple::sfLedgerEntry); + check.setFieldU16(ripple::sfLedgerEntryType, ripple::ltCHECK); + check.setAccountID(ripple::sfAccount, GetAccountIDWithString(account)); + check.setAccountID(ripple::sfDestination, GetAccountIDWithString(dest)); + check.setFieldU32(ripple::sfFlags, 0); + check.setFieldU64(ripple::sfOwnerNode, 0); + check.setFieldU64(ripple::sfDestinationNode, 0); + check.setFieldAmount(ripple::sfSendMax, ripple::STAmount(0, false)); + check.setFieldU32(ripple::sfSequence, 0); + check.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + check.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return check; +} + +ripple::STObject +CreateDepositPreauthLedgerObject( + std::string_view account, + std::string_view auth) +{ + ripple::STObject depositPreauth(ripple::sfLedgerEntry); + depositPreauth.setFieldU16( + ripple::sfLedgerEntryType, ripple::ltDEPOSIT_PREAUTH); + depositPreauth.setAccountID( + ripple::sfAccount, GetAccountIDWithString(account)); + depositPreauth.setAccountID( + ripple::sfAuthorize, GetAccountIDWithString(auth)); + depositPreauth.setFieldU32(ripple::sfFlags, 0); + depositPreauth.setFieldU64(ripple::sfOwnerNode, 0); + depositPreauth.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + depositPreauth.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + return depositPreauth; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h index 8751744d..8c408578 100644 --- a/unittests/util/TestObject.h +++ b/unittests/util/TestObject.h @@ -169,3 +169,25 @@ CreateRippleStateLedgerObject( int highLimit, std::string_view previousTxnId, uint32_t previousTxnSeq); + +ripple::STObject +CreateOfferLedgerObject( + std::string_view account, + int takerGets, + int takerPays, + std::string_view currency, + std::string_view issueId); + +ripple::STObject +CreateTicketLedgerObject(std::string_view rootIndex, uint32_t sequence); + +ripple::STObject +CreateEscrowLedgerObject(std::string_view account, std::string_view dest); + +ripple::STObject +CreateCheckLedgerObject(std::string_view account, std::string_view dest); + +ripple::STObject +CreateDepositPreauthLedgerObject( + std::string_view account, + std::string_view auth);