diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 41b0239fb50..183cd3d86b3 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -681,6 +681,46 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryAmendments() + { + testcase("ledger_entry Request Amendments"); + using namespace test::jtx; + + Env env{*this, envconfig(validator, ""), supported_amendments()}; + env.close(); + + { + // Not Found + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::amendments] = true; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + + auto majorities = getMajorityAmendments(*env.closed()); + for (int i = 0; i <= 256; ++i) + { + env.close(); + majorities = getMajorityAmendments(*env.closed()); + if (!majorities.empty()) + break; + } + + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::amendments] = true; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Amendments); + } + } + void testLedgerEntryCheck() { @@ -1719,6 +1759,27 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryFee() + { + testcase("ledger_entry Request Fee"); + using namespace test::jtx; + + Env env{*this}; + env.close(); + + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::fee] = true; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == jss::FeeSettings); + } + } + void testLedgerEntryOffer() { @@ -2156,6 +2217,201 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryHashes() + { + testcase("ledger_entry Request Hashes"); + using namespace test::jtx; + + Env env{*this, envconfig(validator, ""), supported_amendments()}; + env.close(); + + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::hashes] = true; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == + jss::LedgerHashes); + } + + { + // Not Found + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::hashes][jss::ledger_index] = 5; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + + // proceed to the next flag ledger + for (int i = 0; i <= 256; ++i) + env.close(); + + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::hashes][jss::ledger_index] = 5; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == + jss::LedgerHashes); + } + + { + // Fail + Account const alice{"alice"}; + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::fee][jss::owner] = alice.human(); + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Fail + Account const alice{"alice"}; + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::fee][jss::ledger_index] = alice.human(); + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + } + + void + testLedgerEntryNegativeUNL() + { + testcase("ledger_entry Request NegativeUNL"); + using namespace test::jtx; + + Env env{*this, supported_amendments()}; + env.close(); + + { + // Not found + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nunl] = true; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + + { + // TODO: test for when negativeUNL is set + // Success + // Json::Value jvParams; + // jvParams[jss::ledger_index] = "current"; + // jvParams[jss::nunl] = true; + // auto const jrr = env.rpc( + // "json", "ledger_entry", to_string(jvParams))[jss::result]; + // std::cout << "jrr: " << jrr << std::endl; + // BEAST_EXPECT( + // jrr[jss::node][sfLedgerEntryType.jsonName] == + // jss::NegativeUNL); + } + } + + void + testLedgerEntryNFTokenOffer() + { + testcase("ledger_entry Request NFTokenOffer"); + + using namespace test::jtx; + Env env{*this, supported_amendments()}; + Account const alice{"alice"}; + + env.fund(XRP(10000), alice); + env.close(); + + auto const aliceOfferSeq = env.seq(alice); + uint256 const offerID = keylet::nftoffer(alice, aliceOfferSeq).key; + auto createNFTokenAndOffer = [](test::jtx::Account const& account) { + Json::Value jv; + jv[jss::TransactionType] = jss::NFTokenMint; + jv[jss::Account] = account.human(); + jv[sfNFTokenTaxon.jsonName] = 1; + jv[jss::Amount] = "10"; + return jv; + }; + + env(createNFTokenAndOffer(alice)); + env.close(); + + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer] = to_string(offerID); + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == + jss::NFTokenOffer); + } + { + // Success + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer][jss::owner] = alice.human(); + jvParams[jss::nft_offer][jss::seq] = aliceOfferSeq; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == + jss::NFTokenOffer); + } + + { + // Not found + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer] = to_string(uint256{0}); + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + { + // Not found + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer][jss::owner] = alice.human(); + jvParams[jss::nft_offer][jss::seq] = aliceOfferSeq + 1; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + + { + // Fail + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer][jss::owner] = alice.human(); + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Fail + Account const alice{"alice"}; + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::nft_offer][jss::account] = alice.human(); + jvParams[jss::nft_offer][jss::seq] = aliceOfferSeq + 1; + auto const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + } + void testLedgerEntryDID() { @@ -2272,7 +2528,12 @@ class LedgerRPC_test : public beast::unit_test::suite } // Fields that can handle objects just fine for (auto const& field : - {jss::directory, jss::escrow, jss::offer, jss::ticket, jss::amm}) + {jss::directory, + jss::escrow, + jss::offer, + jss::ticket, + jss::amm, + jss::nft_offer}) { auto const jvParams = makeParams([&field, &injectObject](Json::Value& jvParams) { @@ -2284,6 +2545,34 @@ class LedgerRPC_test : public beast::unit_test::suite checkErrorValue(jrr, "malformedRequest", ""); } + // Fields that can handle boolean values + for (auto const& field : + {jss::amendments, jss::fee, jss::hashes, jss::nunl}) + { + auto const jvParams = makeParams( + [&field](Json::Value& jvParams) { jvParams[field] = false; }); + + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + checkErrorValue(jrr, "malformedRequest", ""); + } + // Fields that can handle only boolean values + for (auto const& field : {jss::amendments, jss::fee, jss::nunl}) + { + for (auto const& inject : {injectObject, injectArray}) + { + auto const jvParams = + makeParams([&field, &inject](Json::Value& jvParams) { + jvParams[field] = inject; + }); + + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + checkErrorValue(jrr, "malformedRequest", ""); + } + } for (auto const& inject : {injectObject, injectArray}) { @@ -2339,6 +2628,22 @@ class LedgerRPC_test : public beast::unit_test::suite else checkErrorValue(jrr, "invalidParams", ""); } + // nft_offer sub-fields + { + auto const jvParams = + makeParams([&inject](Json::Value& jvParams) { + jvParams[jss::nft_offer][jss::owner] = inject; + jvParams[jss::nft_offer][jss::seq] = 99; + }); + + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + if (apiVersion < 2u) + checkErrorValue(jrr, "internal", "Internal error."); + else + checkErrorValue(jrr, "invalidParams", ""); + } // offer sub-fields { auto const jvParams = @@ -3098,16 +3403,21 @@ class LedgerRPC_test : public beast::unit_test::suite testLedgerFullNonAdmin(); testLedgerAccounts(); testLedgerEntryAccountRoot(); + testLedgerEntryAmendments(); testLedgerEntryCheck(); testLedgerEntryCredentials(); testLedgerEntryDepositPreauth(); testLedgerEntryDepositPreauthCred(); testLedgerEntryDirectory(); testLedgerEntryEscrow(); + testLedgerEntryFee(); testLedgerEntryOffer(); testLedgerEntryPayChan(); testLedgerEntryRippleState(); testLedgerEntryTicket(); + testLedgerEntryHashes(); + testLedgerEntryNegativeUNL(); + testLedgerEntryNFTokenOffer(); testLookupLedger(); testNoQueue(); testQueue(); diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 4401b4dacd0..6055b7567a1 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,17 @@ parseAccountRoot(Json::Value const& params, Json::Value& jvResult) return keylet::account(*account).key; } +std::optional +parseAmendments(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isBool() || !params.asBool()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return keylet::amendments().key; +} + std::optional parseCheck(Json::Value const& params, Json::Value& jvResult) { @@ -270,6 +282,43 @@ parseEscrow(Json::Value const& params, Json::Value& jvResult) return keylet::escrow(*id, params[jss::seq].asUInt()).key; } +std::optional +parseFee(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isBool() || !params.asBool()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return keylet::fees().key; +} + +std::optional +parseHashes(Json::Value const& params, Json::Value& jvResult) +{ + if (params.isBool()) + { + if (!params.asBool()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return keylet::skip().key; + } + if (!params.isObject()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + if (!params.isMember(jss::ledger_index) || + !params[jss::ledger_index].isIntegral()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return keylet::skip(params[jss::ledger_index].asUInt()).key; +} + std::optional parseOffer(Json::Value const& params, Json::Value& jvResult) { @@ -382,6 +431,36 @@ parseTicket(Json::Value const& params, Json::Value& jvResult) return getTicketIndex(*id, params[jss::ticket_seq].asUInt()); } +std::optional +parseNFTokenOffer(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isObject()) + { + uint256 uNodeIndex; + if (!uNodeIndex.parseHex(params.asString())) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return uNodeIndex; + } + + if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || + !params[jss::seq].isIntegral()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + + auto const id = parseBase58(params[jss::owner].asString()); + if (!id) + { + jvResult[jss::error] = "malformedAddress"; + return std::nullopt; + } + return keylet::nftoffer(*id, params[jss::seq].asUInt()).key; +} + std::optional parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) { @@ -400,6 +479,17 @@ parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) return std::nullopt; } +std::optional +parseNegativeUNL(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isBool() || !params.asBool()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return keylet::negativeUNL().key; +} + std::optional parseAMM(Json::Value const& params, Json::Value& jvResult) { @@ -833,7 +923,7 @@ doLedgerEntry(RPC::JsonContext& context) static auto ledgerEntryParsers = std::to_array({ {jss::index, parseIndex, ltANY}, {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, - // TODO: add amendments + {jss::amendments, parseAmendments, ltAMENDMENTS}, {jss::amm, parseAMM, ltAMM}, {jss::bridge, parseBridge, ltBRIDGE}, {jss::check, parseCheck, ltCHECK}, @@ -842,12 +932,13 @@ doLedgerEntry(RPC::JsonContext& context) {jss::did, parseDID, ltDID}, {jss::directory, parseDirectory, ltDIR_NODE}, {jss::escrow, parseEscrow, ltESCROW}, - // TODO: add fee, hashes + {jss::fee, parseFee, ltFEE_SETTINGS}, + {jss::hashes, parseHashes, ltLEDGER_HASHES}, {jss::mpt_issuance, parseMPTokenIssuance, ltMPTOKEN_ISSUANCE}, {jss::mptoken, parseMPToken, ltMPTOKEN}, - // TODO: add NFT Offers + {jss::nft_offer, parseNFTokenOffer, ltNFTOKEN_OFFER}, {jss::nft_page, parseNFTokenPage, ltNFTOKEN_PAGE}, - // TODO: add NegativeUNL + {jss::nunl, parseNegativeUNL, ltNEGATIVE_UNL}, {jss::offer, parseOffer, ltOFFER}, {jss::oracle, parseOracle, ltORACLE}, {jss::payment_channel, parsePaymentChannel, ltPAYCHAN},