fix: Allow deleted libXRPL amendments to still be queried via feature (#3011)

This commit is contained in:
Alex Kremer
2026-03-24 15:34:02 +00:00
committed by GitHub
parent 4e85398aed
commit 7238cc8799
4 changed files with 141 additions and 5 deletions

View File

@@ -98,14 +98,26 @@ AmendmentCenter::AmendmentCenter(std::shared_ptr<data::BackendInterface> 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);
}

View File

@@ -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,
};
}
);

View File

@@ -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

View File

@@ -14,6 +14,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <optional>
#include <string>
@@ -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<data::Amendment>{{
.name = Amendments::OwnerPaysFee,
.feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee),
.isSupportedByXRPL = false,
.isSupportedByClio = true,
.isRetired = true,
}};
auto const keys = std::vector<data::AmendmentKey>{Amendments::OwnerPaysFee};
auto const enabled = std::vector<bool>{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<data::Amendment>{{
.name = Amendments::OwnerPaysFee,
.feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee),
.isSupportedByXRPL = false,
.isSupportedByClio = true,
.isRetired = true,
}};
auto const keys = std::vector<data::AmendmentKey>{Amendments::OwnerPaysFee};
auto const enabled = std::vector<bool>{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<data::Amendment>{