diff --git a/src/rpc/common/Validators.h b/src/rpc/common/Validators.h index 699fff33..3525dd4b 100644 --- a/src/rpc/common/Validators.h +++ b/src/rpc/common/Validators.h @@ -302,7 +302,7 @@ class OneOf final public: /** - * @brief Construct the validator with stored options + * @brief Construct the validator with stored options of initializer list * * @param options The list of allowed options */ @@ -310,6 +310,16 @@ public: { } + /** + * @brief Construct the validator with stored options of other container + * + * @param begin,end the range to copy the elements from + */ + template + explicit OneOf(InputIt begin, InputIt end) : options_{begin, end} + { + } + /** * @brief Verify that the JSON value is one of the stored options * diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index 61a5ef0e..46241405 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -25,6 +25,32 @@ namespace RPC { +std::unordered_map const LedgerDataHandler::TYPES_MAP{ + {JS(account), ripple::ltACCOUNT_ROOT}, + {JS(amendments), ripple::ltAMENDMENTS}, + {JS(check), ripple::ltCHECK}, + {JS(deposit_preauth), ripple::ltDEPOSIT_PREAUTH}, + {JS(directory), ripple::ltDIR_NODE}, + {JS(escrow), ripple::ltESCROW}, + {JS(fee), ripple::ltFEE_SETTINGS}, + {JS(hashes), ripple::ltLEDGER_HASHES}, + {JS(offer), ripple::ltOFFER}, + {JS(payment_channel), ripple::ltPAYCHAN}, + {JS(signer_list), ripple::ltSIGNER_LIST}, + {JS(state), ripple::ltRIPPLE_STATE}, + {JS(ticket), ripple::ltTICKET}, + {JS(nft_offer), ripple::ltNFTOKEN_OFFER}, + {JS(nft_page), ripple::ltNFTOKEN_PAGE}}; + +// TODO: should be std::views::keys when clang supports it +std::unordered_set const LedgerDataHandler::TYPES_KEYS = [] { + std::unordered_set keys; + std::transform(TYPES_MAP.begin(), TYPES_MAP.end(), std::inserter(keys, keys.begin()), [](auto const& pair) { + return pair.first; + }); + return keys; +}(); + LedgerDataHandler::Result LedgerDataHandler::process(Input input, Context const& ctx) const { @@ -137,16 +163,20 @@ LedgerDataHandler::process(Input input, Context const& ctx) const { ripple::STLedgerEntry const sle{ripple::SerialIter{object.data(), object.size()}, key}; - if (input.binary) + // note the filter is after limit is applied, same as rippled + if (input.type == ripple::LedgerEntryType::ltANY || sle.getType() == input.type) { - boost::json::object entry; - entry[JS(data)] = ripple::serializeHex(sle); - entry[JS(index)] = ripple::to_string(sle.key()); - output.states.push_back(std::move(entry)); - } - else - { - output.states.push_back(toJson(sle)); + if (input.binary) + { + boost::json::object entry; + entry[JS(data)] = ripple::serializeHex(sle); + entry[JS(index)] = ripple::to_string(sle.key()); + output.states.push_back(std::move(entry)); + } + else + { + output.states.push_back(toJson(sle)); + } } } @@ -212,16 +242,19 @@ tag_invoke(boost::json::value_to_tag, boost::json::val } if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = jv.at(JS(ledger_hash)).as_string().c_str(); + 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 = jv.at(JS(ledger_index)).as_int64(); + input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64(); else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") - input.ledgerIndex = std::stoi(jv.at(JS(ledger_index)).as_string().c_str()); + input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str()); } + if (jsonObject.contains(JS(type))) + input.type = LedgerDataHandler::TYPES_MAP.at(jsonObject.at(JS(type)).as_string().c_str()); + return input; } diff --git a/src/rpc/handlers/LedgerData.h b/src/rpc/handlers/LedgerData.h index 7248f0ca..331bf734 100644 --- a/src/rpc/handlers/LedgerData.h +++ b/src/rpc/handlers/LedgerData.h @@ -24,6 +24,9 @@ #include #include +#include +#include + namespace RPC { /** @@ -42,6 +45,10 @@ class LedgerDataHandler static uint32_t constexpr LIMITBINARY = 2048; static uint32_t constexpr LIMITJSON = 256; + static const std::unordered_map TYPES_MAP; + + static const std::unordered_set TYPES_KEYS; + public: struct Output { @@ -67,6 +74,7 @@ public: std::optional marker; std::optional diffMarker; bool outOfOrder = false; + ripple::LedgerEntryType type = ripple::LedgerEntryType::ltANY; }; using Result = HandlerReturnType; @@ -87,6 +95,14 @@ public: {JS(marker), validation::Type{}, validation::IfType{validation::Uint256HexStringValidator}}, + {JS(type), + validation::WithCustomError{ + validation::Type{}, + Status{ripple::rpcINVALID_PARAMS, "Invalid field 'type', not string."}}, + validation::WithCustomError{ + validation::OneOf(TYPES_KEYS.cbegin(), TYPES_KEYS.cend()), + Status{ripple::rpcINVALID_PARAMS, "Invalid field 'type'."}}}, + }; return rpcSpec; } diff --git a/unittests/rpc/handlers/LedgerDataTest.cpp b/unittests/rpc/handlers/LedgerDataTest.cpp index c6dcad9d..778372fb 100644 --- a/unittests/rpc/handlers/LedgerDataTest.cpp +++ b/unittests/rpc/handlers/LedgerDataTest.cpp @@ -87,6 +87,9 @@ generateTestValuesForParametersTest() "invalidParams", "outOfOrderMarkerNotInt"}, LedgerDataParamTestCaseBundle{"markerNotString", R"({"marker": 123})", "invalidParams", "markerNotString"}, + LedgerDataParamTestCaseBundle{ + "typeNotString", R"({"type": 123})", "invalidParams", "Invalid field 'type', not string."}, + LedgerDataParamTestCaseBundle{"typeNotValid", R"({"type": "xxx"})", "invalidParams", "Invalid field 'type'."}, }; } @@ -213,7 +216,6 @@ TEST_F(RPCLedgerDataHandlerTest, MarkerNotExist) }); } -// no marker TEST_F(RPCLedgerDataHandlerTest, NoMarker) { static auto const ledgerExpected = R"({ @@ -242,19 +244,27 @@ TEST_F(RPCLedgerDataHandlerTest, NoMarker) ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); - auto limit = 10; - std::vector bbs; + // when 'type' not specified, default to all the types + auto limitLine = 5; + auto limitTicket = 5; - EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limit); + std::vector bbs; + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limitLine + limitTicket); ON_CALL(*rawBackendPtr, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2})); - while (limit--) + while (limitLine--) { auto const line = CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); bbs.push_back(line.getSerializer().peekData()); } + while (limitTicket--) + { + auto const ticket = CreateTicketLedgerObject(ACCOUNT, limitTicket); + bbs.push_back(ticket.getSerializer().peekData()); + } + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); @@ -274,6 +284,77 @@ TEST_F(RPCLedgerDataHandlerTest, NoMarker) }); } +TEST_F(RPCLedgerDataHandlerTest, TypeFilter) +{ + static auto const ledgerExpected = R"({ + "accepted":true, + "account_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "close_flags":0, + "close_time":0, + "close_time_resolution":0, + "hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index":"30", + "parent_close_time":0, + "parent_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "seqNum":"30", + "totalCoins":"0", + "total_coins":"0", + "transaction_hash":"0000000000000000000000000000000000000000000000000000000000000000", + "closed":true + })"; + + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(RANGEMIN); // min + mockBackendPtr->updateRange(RANGEMAX); // max + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(RANGEMAX, _)) + .WillByDefault(Return(CreateLedgerInfo(LEDGERHASH, RANGEMAX))); + + auto limitLine = 5; + auto limitTicket = 5; + + std::vector bbs; + EXPECT_CALL(*rawBackendPtr, doFetchSuccessorKey).Times(limitLine + limitTicket); + ON_CALL(*rawBackendPtr, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2})); + + while (limitLine--) + { + auto const line = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + bbs.push_back(line.getSerializer().peekData()); + } + + while (limitTicket--) + { + auto const ticket = CreateTicketLedgerObject(ACCOUNT, limitTicket); + bbs.push_back(ticket.getSerializer().peekData()); + } + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{LedgerDataHandler{mockBackendPtr}}; + auto const req = json::parse(R"({ + "limit":10, + "type":"state" + })"); + + auto output = handler.process(req, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_TRUE(output->as_object().contains("ledger")); + //"close_time_human" 's format depends on platform, might be sightly different + EXPECT_EQ(output->as_object().at("ledger").as_object().erase("close_time_human"), 1); + EXPECT_EQ(output->as_object().at("ledger"), json::parse(ledgerExpected)); + EXPECT_EQ(output->as_object().at("marker").as_string(), INDEX2); + EXPECT_EQ(output->as_object().at("state").as_array().size(), 5); + EXPECT_EQ(output->as_object().at("ledger_hash").as_string(), LEDGERHASH); + EXPECT_EQ(output->as_object().at("ledger_index").as_uint64(), RANGEMAX); + }); +} + TEST_F(RPCLedgerDataHandlerTest, OutOfOrder) { static auto const ledgerExpected = R"({