diff --git a/src/data/AmendmentCenter.cpp b/src/data/AmendmentCenter.cpp index 3bcebb589..2a3051c81 100644 --- a/src/data/AmendmentCenter.cpp +++ b/src/data/AmendmentCenter.cpp @@ -98,14 +98,26 @@ AmendmentCenter::AmendmentCenter(std::shared_ptr const& .name = name, .feature = Amendment::getAmendmentId(name), .isSupportedByXRPL = support != ripple::AmendmentSupport::Unsupported, - .isSupportedByClio = - rg::find(supportedAmendments(), name) != rg::end(supportedAmendments()), + .isSupportedByClio = rg::contains(supportedAmendments(), name), .isRetired = support == ripple::AmendmentSupport::Retired }; }), std::back_inserter(all_) ); + // Mix in amendments that Clio registered but libXRPL no longer knows about (deleted upstream). + for (auto const& name : supportedAmendments()) { + if (not rg::contains(all_, name, &Amendment::name)) { + all_.push_back({ + .name = name, + .feature = Amendment::getAmendmentId(name), + .isSupportedByXRPL = false, + .isSupportedByClio = true, + .isRetired = true, + }); + } + } + for (auto const& am : all_ | vs::filter([](auto const& am) { return am.isSupportedByClio; })) supported_.insert_or_assign(am.name, am); } diff --git a/src/rpc/handlers/Feature.cpp b/src/rpc/handlers/Feature.cpp index 3c152cd35..663593a26 100644 --- a/src/rpc/handlers/Feature.cpp +++ b/src/rpc/handlers/Feature.cpp @@ -64,7 +64,7 @@ FeatureHandler::process(FeatureHandler::Input const& input, Context const& ctx) return Output::Feature{ .name = feature.name, .key = ripple::to_string(feature.feature), - .supported = feature.isSupportedByClio, + .supported = feature.isSupportedByClio and feature.isSupportedByXRPL, }; } ); diff --git a/tests/unit/data/AmendmentCenterTests.cpp b/tests/unit/data/AmendmentCenterTests.cpp index aa2834aa5..5dd1265a0 100644 --- a/tests/unit/data/AmendmentCenterTests.cpp +++ b/tests/unit/data/AmendmentCenterTests.cpp @@ -37,8 +37,9 @@ TEST_F(AmendmentCenterTest, AllAmendmentsFromLibXRPLAreSupported) << "XRPL amendment not supported by Clio: " << name; } - ASSERT_EQ(amendmentCenter.getSupported().size(), ripple::allAmendments().size()); - ASSERT_EQ(amendmentCenter.getAll().size(), ripple::allAmendments().size()); + // We support at least all the amendments currently exposed by libXRPL + ASSERT_GE(amendmentCenter.getSupported().size(), ripple::allAmendments().size()); + ASSERT_GE(amendmentCenter.getAll().size(), ripple::allAmendments().size()); } TEST_F(AmendmentCenterTest, Accessors) @@ -147,6 +148,32 @@ TEST_F(AmendmentCenterTest, IsEnabledReturnsVectorOfFalseWhenNoAmendments) }); } +TEST_F(AmendmentCenterTest, DeletedLibXRPLAmendmentIsNotKnownToLibXRPL) +{ + // OwnerPaysFee was removed from libXRPL in 2.6.0; confirm it's not present upstream + EXPECT_FALSE(ripple::allAmendments().contains(std::string{Amendments::OwnerPaysFee})); +} + +TEST_F(AmendmentCenterTest, DeletedLibXRPLAmendmentIsPresentInGetAllWithCorrectFlags) +{ + auto const& all = amendmentCenter.getAll(); + auto const it = std::ranges::find(all, std::string{Amendments::OwnerPaysFee}, &Amendment::name); + + ASSERT_NE( + it, all.end() + ) << "OwnerPaysFee must be present in getAll() even after libXRPL deleted it"; + EXPECT_FALSE(it->isSupportedByXRPL); + EXPECT_TRUE(it->isSupportedByClio); + EXPECT_TRUE(it->isRetired); +} + +TEST_F(AmendmentCenterTest, DeletedLibXRPLAmendmentIsSupportedByClio) +{ + // Clio still registers OwnerPaysFee so isSupported() and getSupported() must include it + EXPECT_TRUE(amendmentCenter.isSupported(Amendments::OwnerPaysFee)); + EXPECT_TRUE(amendmentCenter.getSupported().contains(std::string{Amendments::OwnerPaysFee})); +} + TEST(AmendmentTest, GenerateAmendmentId) { // https://xrpl.org/known-amendments.html#disallowincoming refer to the published id diff --git a/tests/unit/rpc/handlers/FeatureTests.cpp b/tests/unit/rpc/handlers/FeatureTests.cpp index 144f3cfa3..93f3b72e6 100644 --- a/tests/unit/rpc/handlers/FeatureTests.cpp +++ b/tests/unit/rpc/handlers/FeatureTests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -386,6 +387,102 @@ TEST_F(RPCFeatureHandlerTest, BadFeaturePath) }); } +TEST_F(RPCFeatureHandlerTest, DeletedLibXRPLAmendmentQueryByNameReturnsSupportedFalse) +{ + auto const ownerPaysFeeKey = + ripple::to_string(data::Amendment::getAmendmentId(Amendments::OwnerPaysFee)); + auto const all = std::vector{{ + .name = Amendments::OwnerPaysFee, + .feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee), + .isSupportedByXRPL = false, + .isSupportedByClio = true, + .isRetired = true, + }}; + auto const keys = std::vector{Amendments::OwnerPaysFee}; + auto const enabled = std::vector{false}; + + EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); + EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ)) + .WillOnce(testing::Return(enabled)); + + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader)); + + auto const expectedOutput = fmt::format( + R"JSON({{ + "{}": {{ + "name": "OwnerPaysFee", + "enabled": false, + "supported": false + }}, + "ledger_hash": "{}", + "ledger_index": {}, + "validated": true + }})JSON", + ownerPaysFeeKey, + kLEDGER_HASH, + kSEQ + ); + + runSpawn([this, &expectedOutput](auto yield) { + auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}}; + auto const output = handler.process( + boost::json::parse(R"JSON({"feature": "OwnerPaysFee"})JSON"), Context{yield} + ); + + ASSERT_TRUE(output); + EXPECT_EQ(*output.result, boost::json::parse(expectedOutput)); + }); +} + +TEST_F(RPCFeatureHandlerTest, DeletedLibXRPLAmendmentQueryByHashReturnsSupportedFalse) +{ + auto const ownerPaysFeeKey = + ripple::to_string(data::Amendment::getAmendmentId(Amendments::OwnerPaysFee)); + auto const all = std::vector{{ + .name = Amendments::OwnerPaysFee, + .feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee), + .isSupportedByXRPL = false, + .isSupportedByClio = true, + .isRetired = true, + }}; + auto const keys = std::vector{Amendments::OwnerPaysFee}; + auto const enabled = std::vector{true}; + + EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); + EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ)) + .WillOnce(testing::Return(enabled)); + + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader)); + + auto const expectedOutput = fmt::format( + R"JSON({{ + "{}": {{ + "name": "OwnerPaysFee", + "enabled": true, + "supported": false + }}, + "ledger_hash": "{}", + "ledger_index": {}, + "validated": true + }})JSON", + ownerPaysFeeKey, + kLEDGER_HASH, + kSEQ + ); + + runSpawn([this, &ownerPaysFeeKey, &expectedOutput](auto yield) { + auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}}; + auto const req = + boost::json::parse(fmt::format(R"JSON({{"feature": "{}"}})JSON", ownerPaysFeeKey)); + auto const output = handler.process(req, Context{yield}); + + ASSERT_TRUE(output); + EXPECT_EQ(*output.result, boost::json::parse(expectedOutput)); + }); +} + TEST_F(RPCFeatureHandlerTest, SuccessPathWithMultipleResults) { auto const all = std::vector{