diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 21b0145f43..5389f99be9 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -248,6 +248,13 @@ words: - queuable - Raphson - rcflags + - recognise + - recognises + - recognised + - renormalise + - renormalises + - renormalised + - renormalisation - replayer - rerandomize - rerandomization diff --git a/cmake/scripts/codegen/templates/LedgerEntry.h.mako b/cmake/scripts/codegen/templates/LedgerEntry.h.mako index c799903b21..4e328110da 100644 --- a/cmake/scripts/codegen/templates/LedgerEntry.h.mako +++ b/cmake/scripts/codegen/templates/LedgerEntry.h.mako @@ -2,6 +2,9 @@ #pragma once #include +% if any(field.get('stiSuffix') == 'NUMBER' for field in fields): +#include // IWYU pragma: keep +% endif #include #include #include diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index ffcd025f01..64fca8cd3a 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -291,6 +291,7 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({ {sfHighQualityOut, SoeOptional}, {sfHighSponsor, SoeOptional}, {sfLowSponsor, SoeOptional}, + {sfDust, SoeDefault}, })) /** The ledger object which lists the network's fee settings. diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index cff075e738..d89a3a6cbf 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -236,6 +236,16 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault) +// sfDust is a sub-quantum residual on a trust line: the fraction of a credit +// that could not be represented in sfBalance at the caller-chosen accounting +// scale, parked here so the extended quantity (sfBalance + sfDust) preserves +// its exact value across successive rounded credits. Deliberately WITHOUT +// kSmdNeedsAsset: that flag rounds the field to the asset's representable +// precision via associateAsset(), which is the opposite of this field's +// purpose (it exists to hold value finer than that precision). Do not add +// kSmdNeedsAsset to this entry — see the "Why sfDust" section in +// docs/dust-mechanism.md. +TYPED_SFIELD(sfDust, NUMBER, 18, SField::kSmdDefault) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h index dda1b78e66..a3b2ec1327 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -2,6 +2,7 @@ #pragma once #include +#include // IWYU pragma: keep #include #include #include @@ -291,6 +292,30 @@ public: { return this->sle_->isFieldPresent(sfLowSponsor); } + + /** + * @brief Get sfDust (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDust() const + { + if (hasDust()) + return this->sle_->at(sfDust); + return std::nullopt; + } + + /** + * @brief Check if sfDust is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDust() const + { + return this->sle_->isFieldPresent(sfDust); + } }; /** @@ -482,6 +507,17 @@ public: return *this; } + /** + * @brief Set sfDust (SoeDefault) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setDust(std::decay_t const& value) + { + object_[sfDust] = value; + return *this; + } + /** * @brief Build and return the completed RippleState wrapper. * @param index The ledger entry index. diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp index de0769793b..0f791eb0e5 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp @@ -33,6 +33,7 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) auto const highQualityOutValue = canonical_UINT32(); auto const highSponsorValue = canonical_ACCOUNT(); auto const lowSponsorValue = canonical_ACCOUNT(); + auto const dustValue = canonical_NUMBER(); RippleStateBuilder builder{ balanceValue, @@ -50,6 +51,7 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) builder.setHighQualityOut(highQualityOutValue); builder.setHighSponsor(highSponsorValue); builder.setLowSponsor(lowSponsorValue); + builder.setDust(dustValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -154,6 +156,14 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLowSponsor()); } + { + auto const& expected = dustValue; + auto const actualOpt = entry.getDust(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfDust"); + EXPECT_TRUE(entry.hasDust()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -180,6 +190,7 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) auto const highQualityOutValue = canonical_UINT32(); auto const highSponsorValue = canonical_ACCOUNT(); auto const lowSponsorValue = canonical_ACCOUNT(); + auto const dustValue = canonical_NUMBER(); auto sle = std::make_shared(RippleState::entryType, index); @@ -196,6 +207,7 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) sle->at(sfHighQualityOut) = highQualityOutValue; sle->at(sfHighSponsor) = highSponsorValue; sle->at(sfLowSponsor) = lowSponsorValue; + sle->at(sfDust) = dustValue; RippleStateBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -360,6 +372,19 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLowSponsor"); } + { + auto const& expected = dustValue; + + auto const fromSleOpt = entryFromSle.getDust(); + auto const fromBuilderOpt = entryFromBuilder.getDust(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfDust"); + expectEqualField(expected, *fromBuilderOpt, "sfDust"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -438,5 +463,7 @@ TEST(RippleStateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getHighSponsor().has_value()); EXPECT_FALSE(entry.hasLowSponsor()); EXPECT_FALSE(entry.getLowSponsor().has_value()); + EXPECT_FALSE(entry.hasDust()); + EXPECT_FALSE(entry.getDust().has_value()); } }