diff --git a/CMakeLists.txt b/CMakeLists.txt index 9307409f..93a61ec1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/LedgerRange.cpp src/rpc/ngHandlers/BookOffers.cpp src/rpc/ngHandlers/TransactionEntry.cpp + src/rpc/ngHandlers/NoRippleCheck.cpp src/rpc/ngHandlers/NFTInfo.cpp ## RPC Methods # Account @@ -136,6 +137,7 @@ if(BUILD_TESTS) unittests/rpc/handlers/LedgerRangeTest.cpp unittests/rpc/handlers/BookOffersTest.cpp unittests/rpc/handlers/TransactionEntryTest.cpp + unittests/rpc/handlers/NoRippleCheckTest.cpp unittests/rpc/handlers/NFTInfoTest.cpp) include(CMake/deps/gtest.cmake) diff --git a/src/rpc/ngHandlers/NoRippleCheck.cpp b/src/rpc/ngHandlers/NoRippleCheck.cpp new file mode 100644 index 00000000..00e7d221 --- /dev/null +++ b/src/rpc/ngHandlers/NoRippleCheck.cpp @@ -0,0 +1,220 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { + +NoRippleCheckHandler::Result +NoRippleCheckHandler::process( + NoRippleCheckHandler::Input input, + boost::asio::yield_context& yield) const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq( + *sharedPtrBackend_, + yield, + input.ledgerHash, + input.ledgerIndex, + range->maxSequence); + + if (auto status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + auto const accountID = RPC::accountFromStringStrict(input.account); + auto const keylet = ripple::keylet::account(*accountID).key; + auto const accountObj = + sharedPtrBackend_->fetchLedgerObject(keylet, lgrInfo.seq, yield); + if (!accountObj) + return Error{RPC::Status{ + RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + + ripple::SerialIter it{accountObj->data(), accountObj->size()}; + ripple::SLE sle{it, keylet}; + auto accountSeq = sle.getFieldU32(ripple::sfSequence); + + bool const bDefaultRipple = + sle.getFieldU32(ripple::sfFlags) & ripple::lsfDefaultRipple; + + auto const fees = input.transactions + ? sharedPtrBackend_->fetchFees(lgrInfo.seq, yield) + : std::nullopt; + + auto output = NoRippleCheckHandler::Output(); + if (input.transactions) + output.transactions.emplace(boost::json::array()); + + auto const getBaseTx = [&](ripple::AccountID const& accountID, + std::uint32_t accountSeq) { + boost::json::object tx; + tx[JS(Sequence)] = accountSeq; + tx[JS(Account)] = ripple::toBase58(accountID); + tx[JS(Fee)] = RPC::toBoostJson(fees->units.jsonClipped()); + return tx; + }; + + if (bDefaultRipple && !input.roleGateway) + { + output.problems.push_back( + "You appear to have set your default ripple flag even though " + "you " + "are not a gateway. This is not recommended unless you are " + "experimenting"); + } + else if (input.roleGateway && !bDefaultRipple) + { + output.problems.push_back( + "You should immediately set your default ripple flag"); + if (input.transactions) + { + auto tx = getBaseTx(*accountID, accountSeq++); + tx[JS(TransactionType)] = "AccountSet"; + tx[JS(SetFlag)] = ripple::asfDefaultRipple; + output.transactions->push_back(tx); + } + } + auto limit = input.limit; + RPC::ngTraverseOwnedNodes( + *sharedPtrBackend_, + *accountID, + lgrInfo.seq, + std::numeric_limits::max(), + {}, + yield, + [&](ripple::SLE&& ownedItem) { + // don't push to result if limit is reached + if (limit != 0 && ownedItem.getType() == ripple::ltRIPPLE_STATE) + { + bool const bLow = accountID == + ownedItem.getFieldAmount(ripple::sfLowLimit).getIssuer(); + + bool const bNoRipple = ownedItem.getFieldU32(ripple::sfFlags) & + (bLow ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); + + std::string problem; + bool needFix = false; + if (bNoRipple && input.roleGateway) + { + problem = "You should clear the no ripple flag on your "; + needFix = true; + } + else if (!bNoRipple && !input.roleGateway) + { + problem = + "You should probably set the no ripple flag on " + "your "; + needFix = true; + } + if (needFix) + { + limit--; + ripple::AccountID peer = + ownedItem + .getFieldAmount( + bLow ? ripple::sfHighLimit : ripple::sfLowLimit) + .getIssuer(); + ripple::STAmount peerLimit = ownedItem.getFieldAmount( + bLow ? ripple::sfHighLimit : ripple::sfLowLimit); + problem += fmt::format( + "{} line to {}", + to_string(peerLimit.getCurrency()), + to_string(peerLimit.getIssuer())); + output.problems.emplace_back(problem); + if (input.transactions) + { + ripple::STAmount limitAmount(ownedItem.getFieldAmount( + bLow ? ripple::sfLowLimit : ripple::sfHighLimit)); + limitAmount.setIssuer(peer); + auto tx = getBaseTx(*accountID, accountSeq++); + tx[JS(TransactionType)] = "TrustSet"; + tx[JS(LimitAmount)] = RPC::toBoostJson( + limitAmount.getJson(ripple::JsonOptions::none)); + tx[JS(Flags)] = bNoRipple ? ripple::tfClearNoRipple + : ripple::tfSetNoRipple; + output.transactions->push_back(tx); + } + } + } + return true; + }); + + output.ledgerIndex = lgrInfo.seq; + output.ledgerHash = ripple::strHex(lgrInfo.hash); + return output; +} + +NoRippleCheckHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + NoRippleCheckHandler::Input input; + input.account = jsonObject.at(JS(account)).as_string().c_str(); + input.roleGateway = jsonObject.at(JS(role)).as_string() == "gateway"; + if (jsonObject.contains(JS(limit))) + { + input.limit = jsonObject.at(JS(limit)).as_int64(); + } + if (jsonObject.contains(JS(transactions))) + { + input.transactions = jsonObject.at(JS(transactions)).as_bool(); + } + 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()); + } + } + + return input; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + NoRippleCheckHandler::Output const& output) +{ + auto obj = boost::json::object{ + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {"problems", output.problems}}; + if (output.transactions) + { + obj.emplace(JS(transactions), *(output.transactions)); + } + jv = std::move(obj); +} +} // namespace RPCng diff --git a/src/rpc/ngHandlers/NoRippleCheck.h b/src/rpc/ngHandlers/NoRippleCheck.h new file mode 100644 index 00000000..608816cc --- /dev/null +++ b/src/rpc/ngHandlers/NoRippleCheck.h @@ -0,0 +1,100 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +namespace RPCng { +class NoRippleCheckHandler +{ + std::shared_ptr sharedPtrBackend_; + +public: + struct Output + { + std::string ledgerHash; + uint32_t ledgerIndex; + std::vector problems; + // TODO: use better type than json + std::optional transactions; + bool validated = true; + }; + + struct Input + { + std::string account; + bool roleGateway = false; + std::optional ledgerHash; + std::optional ledgerIndex; + uint32_t limit = 300; + bool transactions = false; + }; + + using Result = RPCng::HandlerReturnType; + + NoRippleCheckHandler( + std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static auto const rpcSpec = RpcSpec{ + {JS(account), validation::Required{}, validation::AccountValidator}, + {JS(role), + validation::Required{}, + validation::WithCustomError{ + validation::OneOf{"gateway", "user"}, + RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + "role field is invalid"}}}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(limit), + validation::Type(), + validation::Between{1, 500}}, + {JS(transactions), 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, + NoRippleCheckHandler::Output const& output); + +NoRippleCheckHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); +} // namespace RPCng diff --git a/unittests/rpc/handlers/NoRippleCheckTest.cpp b/unittests/rpc/handlers/NoRippleCheckTest.cpp new file mode 100644 index 00000000..c37a8125 --- /dev/null +++ b/unittests/rpc/handlers/NoRippleCheckTest.cpp @@ -0,0 +1,737 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +using namespace RPCng; +namespace json = boost::json; +using namespace testing; + +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto INDEX1 = + "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto INDEX2 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto TXNID = + "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"; + +class RPCNoRippleCheckTest : public HandlerBaseTest +{ +}; + +struct NoRippleParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct NoRippleCheckParameterTest + : public RPCNoRippleCheckTest, + 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{ + NoRippleParamTestCaseBundle{ + "AccountNotExists", + R"({ + "role": "gateway" + })", + "invalidParams", + "Required field 'account' missing"}, + NoRippleParamTestCaseBundle{ + "AccountNotString", + R"({ + "account": 123, + "role": "gateway" + })", + "invalidParams", + "accountNotString"}, + NoRippleParamTestCaseBundle{ + "InvalidAccount", + R"({ + "account": "123", + "role": "gateway" + })", + "invalidParams", + "accountMalformed"}, + NoRippleParamTestCaseBundle{ + "InvalidRole", + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "role": "notrole" + })", + "invalidParams", + "role field is invalid"}, + NoRippleParamTestCaseBundle{ + "RoleNotExists", + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + })", + "invalidParams", + "Required field 'role' missing"}, + NoRippleParamTestCaseBundle{ + "LimitNotInRange", + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "role": "gateway", + "limit": 501 + })", + "invalidParams", + "Invalid parameters."}, + NoRippleParamTestCaseBundle{ + "LimitNotInt", + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "role": "gateway", + "limit": "gg" + })", + "invalidParams", + "Invalid parameters."}, + NoRippleParamTestCaseBundle{ + "TransactionsNotBool", + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "role": "gateway", + "transactions": "gg" + })", + "invalidParams", + "Invalid parameters."}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCNoRippleCheckGroup1, + NoRippleCheckParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + NoRippleCheckParameterTest::NameGenerator{}); + +TEST_P(NoRippleCheckParameterTest, InvalidParams) +{ + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{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(RPCNoRippleCheckTest, LedgerNotExist) +{ + 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::nullopt)); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "role": "gateway", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + auto const handler = AnyHandler{NoRippleCheckHandler{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(RPCNoRippleCheckTest, AccountNotExist) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return emtpy + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "gateway" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "actNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound"); + }); +} + +TEST_F( + RPCNoRippleCheckTest, + NormalPathRoleUserDefaultRippleSetTrustLineNoRippleSet) +{ + static auto constexpr seq = 30; + static auto constexpr expectedOutput = + R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "problems": + [ + "You appear to have set your default ripple flag even though you are not a gateway. This is not recommended unless you are experimenting" + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(seq); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault( + Return(CreateAccountRootObject( + ACCOUNT, ripple::lsfDefaultRipple, 2, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "user" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +} + +TEST_F( + RPCNoRippleCheckTest, + NormalPathRoleUserDefaultRippleUnsetTrustLineNoRippleUnSet) +{ + static auto constexpr seq = 30; + static auto constexpr expectedOutput = + R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "problems":[ + "You should probably set the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "You should probably set the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(seq); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault( + Return(CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "user" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +} + +TEST_F( + RPCNoRippleCheckTest, + NormalPathRoleGatewayDefaultRippleSetTrustLineNoRippleSet) +{ + static auto constexpr seq = 30; + static auto constexpr expectedOutput = + R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "problems": + [ + "You should clear the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "You should clear the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(seq); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault( + Return(CreateAccountRootObject( + ACCOUNT, ripple::lsfDefaultRipple, 2, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "gateway" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +} + +TEST_F( + RPCNoRippleCheckTest, + NormalPathRoleGatewayDefaultRippleUnsetTrustLineNoRippleUnset) +{ + static auto constexpr seq = 30; + static auto constexpr expectedOutput = + R"({ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "problems": + [ + "You should immediately set your default ripple flag" + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(seq); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault( + Return(CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "gateway" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +} + +TEST_F(RPCNoRippleCheckTest, NormalPathLimit) +{ + constexpr auto seq = 30; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault( + Return(CreateAccountRootObject( + ACCOUNT, ripple::lsfDefaultRipple, 2, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "gateway", + "limit": 1 + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(output->as_object().at("problems").as_array().size(), 1); + }); +} + +TEST_F(RPCNoRippleCheckTest, NormalPathTransactions) +{ + constexpr auto seq = 30; + constexpr auto transactionSeq = 123; + const auto expectedOutput = fmt::format( + R"({{ + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":30, + "problems":[ + "You should immediately set your default ripple flag", + "You should clear the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "You should clear the no ripple flag on your USD line to rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun" + ], + "transactions":[ + {{ + "Sequence":{}, + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":4, + "TransactionType":"AccountSet", + "SetFlag":8 + }}, + {{ + "Sequence":{}, + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":4, + "TransactionType":"TrustSet", + "LimitAmount":{{ + "currency":"USD", + "issuer":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value":"10" + }}, + "Flags":{} + }}, + {{ + "Sequence":{}, + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":4, + "TransactionType":"TrustSet", + "LimitAmount":{{ + "currency":"USD", + "issuer":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "value":"10" + }}, + "Flags":{} + }} + ] + }})", + transactionSeq, + transactionSeq + 1, + ripple::tfClearNoRipple, + transactionSeq + 2, + ripple::tfClearNoRipple); + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(seq); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return valid account with DefaultRippleSet flag + + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(CreateAccountRootObject( + ACCOUNT, 0, transactionSeq, 200, 2, INDEX1, 2) + .getSerializer() + .peekData())); + auto const ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + auto const ownerDirKk = + ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(ripple::keylet::fees().key, seq, _)) + .WillByDefault(Return(CreateFeeSettingBlob(1, 2, 3, 4, 0))); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ISSUER, + 100, + ACCOUNT, + 10, + ACCOUNT2, + 20, + TXNID, + 123, + ripple::lsfLowNoRipple); + + std::vector bbs; + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}", + "role": "gateway", + "transactions": true + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NoRippleCheckHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(*output, json::parse(expectedOutput)); + }); +}