refactor: Replace SLE helper inheritance with view-parameterized templates

This commit is contained in:
Mayukha Vadari
2026-03-24 14:41:19 -07:00
parent e9c4ed6a74
commit 5b0b1ff1f6
9 changed files with 222 additions and 161 deletions

View File

@@ -19,6 +19,11 @@
namespace xrpl {
// Forward declarations for SLE wrappers
template <typename ViewT>
class AccountRoot;
using ReadOnlyAccountRoot = AccountRoot<ReadView>;
enum class SkipEntry : bool { No = false, Yes };
//------------------------------------------------------------------------------
@@ -157,7 +162,7 @@ canWithdraw(
ReadView const& view,
AccountID const& from,
AccountID const& to,
AccountRoot const& toWrapped,
ReadOnlyAccountRoot const& toWrapped,
STAmount const& amount,
bool hasDestinationTag);

View File

@@ -18,27 +18,60 @@
namespace xrpl {
/**
* Read-only wrapper for AccountRoot ledger entries.
* View-parameterized wrapper for AccountRoot ledger entries.
*
* Provides read-only access to account data.
* AccountRoot<ReadView> — read-only access to account data
* AccountRoot<ApplyView> — read-write access, with insert/update/erase
* and domain-specific write methods
*/
class AccountRoot : public ReadOnlySLE
template <typename ViewT>
class AccountRoot : public SLEBase<ViewT>
{
protected:
static constexpr bool is_writable = SLEBase<ViewT>::is_writable;
AccountID const id_;
public:
/** Constructor for read-only context */
AccountRoot(AccountID const& id, ReadView const& view)
: ReadOnlySLE(view.read(keylet::account(id)), view), id_(id)
requires(!is_writable)
: SLEBase<ViewT>(view.read(keylet::account(id)), view), id_(id)
{
}
/** Constructor for writable context */
AccountRoot(AccountID const& id, ApplyView& view)
requires is_writable
: SLEBase<ViewT>(keylet::account(id), view), id_(id)
{
}
/** Converting constructor: writable → read-only. */
template <WritableView OtherViewT>
AccountRoot(AccountRoot<OtherViewT> const& other)
requires(!is_writable)
: SLEBase<ViewT>(other), id_(other.id())
{
}
/** Create an AccountRoot backed by a brand-new SLE
* (not yet inserted into the view).
*/
[[nodiscard]] static AccountRoot
makeNew(AccountID const& id, ApplyView& view)
requires is_writable
{
return AccountRoot(id, view, std::make_shared<SLE>(keylet::account(id)));
}
AccountID const&
id() const
{
return id_;
}
// --- Read-only domain methods (available on both specializations) ---
/** Check if the issuer has the global freeze flag set.
@return true if the account has global freeze set
*/
@@ -94,52 +127,36 @@ public:
{
return id_ == other;
}
};
/**
* Writable wrapper for AccountRoot ledger entries.
*
* Provides read-write access to account data.
* Inherits from AccountRoot to reuse read-only methods,
* and adds write capabilities.
*/
class WritableAccountRoot : public AccountRoot, public WritableSLE
{
public:
WritableAccountRoot(AccountID const& id, ApplyView& view)
: AccountRoot(id, view), WritableSLE(keylet::account(id), view)
{
}
/** Create a WritableAccountRoot backed by a brand-new SLE
* (not yet inserted into the view).
*/
[[nodiscard]] static WritableAccountRoot
makeNew(AccountID const& id, ApplyView& view)
{
return WritableAccountRoot(id, view, std::make_shared<SLE>(keylet::account(id)));
}
private:
// This is a private constructor only used by `makeNew`
WritableAccountRoot(AccountID const& id, ApplyView& view, std::shared_ptr<SLE> sle)
: AccountRoot(id, view), WritableSLE(std::move(sle), view)
{
insert();
}
public:
// Resolve ambiguity: use writable operator-> for non-const, read-only for const
using WritableSLE::operator->;
using AccountRoot::operator->;
using WritableSLE::operator*;
using AccountRoot::operator*;
// --- Write-only domain methods (compile-time gated) ---
/** Adjust the owner count up or down. */
void
adjustOwnerCount(std::int32_t amount, beast::Journal j);
adjustOwnerCount(std::int32_t amount, beast::Journal j)
requires is_writable;
private:
// Private constructor only used by `makeNew`
AccountRoot(AccountID const& id, ApplyView& view, std::shared_ptr<SLE> sle)
requires is_writable
: SLEBase<ViewT>(std::move(sle), view), id_(id)
{
this->insert();
}
};
// CTAD deduction guide — bare AccountRoot(id, view) always deduces read-only.
// For writable access, use WritableAccountRoot(id, applyView) explicitly.
AccountRoot(AccountID const&, ReadView const&) -> AccountRoot<ReadView>;
// Backward-compatible aliases
using ReadOnlyAccountRoot = AccountRoot<ReadView>;
using WritableAccountRoot = AccountRoot<ApplyView>;
// Explicit instantiation declarations (definitions in .cpp)
extern template class AccountRoot<ReadView>;
extern template class AccountRoot<ApplyView>;
/** Generate a pseudo-account address from a pseudo owner key.
@param pseudoOwnerKey The key to generate the address from
@return The generated account ID
@@ -177,7 +194,7 @@ isPseudoAccount(
AccountID const& accountId,
std::set<SField const*> const& pseudoFieldFilter = {})
{
AccountRoot const acct(accountId, view);
AccountRoot<ReadView> const acct(accountId, view);
if (!acct)
return false;
return acct.isPseudoAccount(pseudoFieldFilter);

View File

@@ -74,7 +74,7 @@ verifyDepositPreauth(
STTx const& tx,
ApplyView& view,
AccountID const& src,
AccountRoot const& dst,
ReadOnlyAccountRoot const& dst,
beast::Journal j);
} // namespace xrpl

View File

@@ -4,33 +4,53 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <concepts>
#include <memory>
#include <stdexcept>
#include <type_traits>
namespace xrpl {
// Concept to distinguish read-only vs writable view types
template <typename V>
concept WritableView = std::derived_from<V, ApplyView>;
/**
* Read-only base class for all ledger entry view classes.
* View-parameterized base class for all ledger entry wrappers.
*
* Provides common functionality for existence checking and raw SLE read access.
* Supports read-only (ReadView) contexts.
* SLEBase<ReadView> — read-only: holds shared_ptr<SLE const> + ReadView const&
* SLEBase<ApplyView> — writable: holds shared_ptr<SLE> + ApplyView& + Keylet,
* plus insert/update/erase operations
*
* Write-only members are gated by `requires` clauses, providing compile-time
* guarantees that read-only wrappers cannot mutate state.
*
* Derived classes should provide domain-specific accessors that hide
* implementation details of the underlying ledger entry format.
*/
class ReadOnlySLE
template <typename ViewT>
class SLEBase
{
public:
virtual ~ReadOnlySLE() = default;
static constexpr bool is_writable = WritableView<ViewT>;
// Copy/move constructors are fine (reference can be initialized from another)
ReadOnlySLE(ReadOnlySLE const&) = default;
ReadOnlySLE(ReadOnlySLE&&) = default;
// Assignment operators are deleted (cannot rebind reference members)
ReadOnlySLE&
operator=(ReadOnlySLE const&) = delete;
ReadOnlySLE&
operator=(ReadOnlySLE&&) = delete;
// SLE pointer type: mutable for writable views, const for read-only
using sle_ptr_type =
std::conditional_t<is_writable, std::shared_ptr<SLE>, std::shared_ptr<SLE const>>;
// View reference type: ApplyView& for writable, ReadView const& for read-only
using view_ref_type = std::conditional_t<is_writable, ApplyView&, ReadView const&>;
virtual ~SLEBase() = default;
SLEBase(SLEBase const&) = default;
SLEBase(SLEBase&&) = default;
SLEBase&
operator=(SLEBase const&) = delete;
SLEBase&
operator=(SLEBase&&) = delete;
// --- Common interface (always available) ---
/** Returns true if the ledger entry exists */
bool
@@ -46,155 +66,163 @@ public:
return exists();
}
/** Returns the underlying SLE for read access (always available) */
std::shared_ptr<SLE const> const&
/** Returns the underlying SLE for read access */
std::shared_ptr<SLE const>
sle() const
{
return sle_;
}
/** Returns the read view (always available) */
/** Returns the read view (always available; ApplyView inherits ReadView) */
ReadView const&
readView() const
{
return readView_;
return view_;
}
/** Const dereference operators (always available) */
STLedgerEntry const*
operator->() const
{
XRPL_ASSERT(exists(), "xrpl::ReadOnlySLE::operator-> : exists");
XRPL_ASSERT(exists(), "xrpl::SLEBase::operator-> : exists");
return sle_.get();
}
STLedgerEntry const&
operator*() const
{
XRPL_ASSERT(exists(), "xrpl::ReadOnlySLE::operator* : exists");
XRPL_ASSERT(exists(), "xrpl::SLEBase::operator* : exists");
return *sle_;
}
protected:
// Default constructor is deleted (cannot leave reference uninitialized)
ReadOnlySLE() = delete;
/** Constructor for read-only context (ReadView) */
explicit ReadOnlySLE(std::shared_ptr<SLE const> sle, ReadView const& view)
: sle_(std::move(sle)), readView_(view)
{
}
std::shared_ptr<SLE const> sle_; // Always valid (const view)
ReadView const& readView_; // Always valid
};
/**
* Writable base class for all ledger entry view classes.
*
* Extends ReadOnlySLE with write access capabilities.
* Supports read-write (ApplyView) contexts.
*
* Derived classes should provide domain-specific accessors that hide
* implementation details of the underlying ledger entry format.
*/
class WritableSLE
{
public:
virtual ~WritableSLE() = default;
// Copy/move constructors are fine (reference can be initialized from another)
WritableSLE(WritableSLE const&) = default;
WritableSLE(WritableSLE&&) = default;
// Assignment operators are deleted (cannot rebind reference members)
WritableSLE&
operator=(WritableSLE const&) = delete;
WritableSLE&
operator=(WritableSLE&&) = delete;
// --- Writable interface (compile-time gated) ---
/** Returns a mutable SLE for write operations */
std::shared_ptr<SLE> const&
sle_ptr_type const&
mutableSle() const
requires is_writable
{
return mutableSle_;
return sle_;
}
/** Returns true if this wrapper supports write operations */
bool
canModify() const
requires is_writable
{
return mutableSle_ != nullptr;
return sle_ != nullptr;
}
/** Returns the apply view for write operations */
ApplyView&
applyView() const
requires is_writable
{
return applyView_;
return view_;
}
/** Mutable dereference operators */
STLedgerEntry*
operator->()
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::WritableSLE::operator-> : can modify");
return mutableSle_.get();
XRPL_ASSERT(canModify(), "xrpl::SLEBase::operator-> : can modify");
return sle_.get();
}
STLedgerEntry&
operator*()
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::WritableSLE::operator* : can modify");
return *mutableSle_;
XRPL_ASSERT(canModify(), "xrpl::SLEBase::operator* : can modify");
return *sle_;
}
void
insert()
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::WritableSLE::insert : can modify");
applyView_.insert(mutableSle_);
XRPL_ASSERT(canModify(), "xrpl::SLEBase::insert : can modify");
view_.insert(sle_);
}
void
erase()
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::WritableSLE::erase : can modify");
applyView_.erase(mutableSle_);
XRPL_ASSERT(canModify(), "xrpl::SLEBase::erase : can modify");
view_.erase(sle_);
}
void
update()
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::WritableSLE::update : can modify");
applyView_.update(mutableSle_);
XRPL_ASSERT(canModify(), "xrpl::SLEBase::update : can modify");
view_.update(sle_);
}
void
newSLE()
requires is_writable
{
XRPL_ASSERT(!canModify(), "xrpl::WritableSLE::newSLE : mutableSle_ is not null");
mutableSle_ = std::make_shared<SLE>(key_);
XRPL_ASSERT(!canModify(), "xrpl::SLEBase::newSLE : sle_ is not null");
sle_ = std::make_shared<SLE>(key_);
}
protected:
// Default constructor is deleted (cannot leave reference uninitialized)
WritableSLE() = delete;
SLEBase() = delete;
/** Constructor for read-write context (ApplyView) */
explicit WritableSLE(std::shared_ptr<SLE> sle, ApplyView& view)
: applyView_(view)
/** Constructor for read-only context */
explicit SLEBase(std::shared_ptr<SLE const> sle, ReadView const& view)
requires(!is_writable)
: view_(view), sle_(std::move(sle))
{
}
/** Converting constructor: writable → read-only.
* Enables implicit conversion from SLEBase<ApplyView> to
* SLEBase<ReadView>, so functions taking ReadOnlySLE const& can
* accept WritableSLE.
*/
template <WritableView OtherViewT>
SLEBase(SLEBase<OtherViewT> const& other)
requires(!is_writable)
: view_(other.readView()), sle_(other.sle())
{
}
/** Constructor for writable context (from existing SLE) */
explicit SLEBase(std::shared_ptr<SLE> sle, ApplyView& view)
requires is_writable
: view_(view)
, key_(sle ? Keylet(sle->getType(), sle->key()) : Keylet(ltANY, uint256{}))
, mutableSle_(std::move(sle))
, sle_(std::move(sle))
{
}
/** Constructor for read-write context (ApplyView) */
explicit WritableSLE(Keylet const& key, ApplyView& view)
: applyView_(view), key_(key), mutableSle_(applyView_.peek(key))
/** Constructor for writable context (peek from view by keylet) */
explicit SLEBase(Keylet const& key, ApplyView& view)
requires is_writable
: view_(view), key_(key), sle_(view_.peek(key))
{
}
ApplyView& applyView_; // ApplyView for write contexts (first for init order)
Keylet const key_;
std::shared_ptr<SLE> mutableSle_; // Mutable SLE for write contexts
view_ref_type view_;
// Keylet is only meaningful for writable views, but we conditionally
// include it to avoid wasting space in read-only wrappers.
struct Empty
{
};
[[no_unique_address]]
std::conditional_t<is_writable, Keylet, Empty> key_{};
sle_ptr_type sle_;
};
// Backward-compatible aliases
using ReadOnlySLE = SLEBase<ReadView>;
using WritableSLE = SLEBase<ApplyView>;
} // namespace xrpl

View File

@@ -357,7 +357,7 @@ canWithdraw(
ReadView const& view,
AccountID const& from,
AccountID const& to,
AccountRoot const& toWrapped,
ReadOnlyAccountRoot const& toWrapped,
STAmount const& amount,
bool hasDestinationTag)
{

View File

@@ -11,12 +11,13 @@
namespace xrpl {
template <typename ViewT>
bool
AccountRoot::isGlobalFrozen() const
AccountRoot<ViewT>::isGlobalFrozen() const
{
if (!exists())
if (!this->exists())
return false;
return sle_->isFlag(lsfGlobalFreeze);
return this->sle_->isFlag(lsfGlobalFreeze);
}
// An owner count cannot be negative. If adjustment would cause a negative
@@ -61,23 +62,24 @@ confineOwnerCount(
return adjusted;
}
template <typename ViewT>
XRPAmount
AccountRoot::xrpLiquid(std::int32_t ownerCountAdj, beast::Journal j) const
AccountRoot<ViewT>::xrpLiquid(std::int32_t ownerCountAdj, beast::Journal j) const
{
if (!exists())
if (!this->exists())
return beast::zero;
// Return balance minus reserve
std::uint32_t const ownerCount = confineOwnerCount(
readView_.ownerCountHook(id_, sle_->getFieldU32(sfOwnerCount)), ownerCountAdj);
this->readView().ownerCountHook(id_, this->sle_->getFieldU32(sfOwnerCount)), ownerCountAdj);
// Pseudo-accounts have no reserve requirement
auto const reserve =
isPseudoAccount() ? XRPAmount{0} : readView_.fees().accountReserve(ownerCount);
this->isPseudoAccount() ? XRPAmount{0} : this->readView().fees().accountReserve(ownerCount);
auto const fullBalance = sle_->getFieldAmount(sfBalance);
auto const fullBalance = this->sle_->getFieldAmount(sfBalance);
auto const balance = readView_.balanceHook(id_, xrpAccount(), fullBalance);
auto const balance = this->readView().balanceHook(id_, xrpAccount(), fullBalance);
STAmount const amount = (balance < reserve) ? STAmount{0} : balance - reserve;
@@ -90,26 +92,29 @@ AccountRoot::xrpLiquid(std::int32_t ownerCountAdj, beast::Journal j) const
return amount.xrp();
}
template <typename ViewT>
Rate
AccountRoot::transferRate() const
AccountRoot<ViewT>::transferRate() const
{
if (sle_ && sle_->isFieldPresent(sfTransferRate))
return Rate{sle_->getFieldU32(sfTransferRate)};
if (this->sle_ && this->sle_->isFieldPresent(sfTransferRate))
return Rate{this->sle_->getFieldU32(sfTransferRate)};
return parityRate;
}
template <typename ViewT>
void
WritableAccountRoot::adjustOwnerCount(std::int32_t amount, beast::Journal j)
AccountRoot<ViewT>::adjustOwnerCount(std::int32_t amount, beast::Journal j)
requires is_writable
{
XRPL_ASSERT(canModify(), "xrpl::adjustOwnerCount : can modify");
XRPL_ASSERT(this->canModify(), "xrpl::adjustOwnerCount : can modify");
XRPL_ASSERT(amount, "xrpl::adjustOwnerCount : nonzero amount input");
std::uint32_t const current{mutableSle_->getFieldU32(sfOwnerCount)};
AccountID const id = (*mutableSle_)[sfAccount];
std::uint32_t const current{this->sle_->getFieldU32(sfOwnerCount)};
AccountID const id = (*this->sle_)[sfAccount];
std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j);
applyView_.adjustOwnerCountHook(id_, current, adjusted);
mutableSle_->at(sfOwnerCount) = adjusted;
update();
this->applyView().adjustOwnerCountHook(id_, current, adjusted);
this->sle_->at(sfOwnerCount) = adjusted;
this->update();
}
AccountID
@@ -161,17 +166,18 @@ getPseudoAccountFields()
return pseudoFields;
}
template <typename ViewT>
[[nodiscard]] bool
AccountRoot::isPseudoAccount(std::set<SField const*> const& pseudoFieldFilter) const
AccountRoot<ViewT>::isPseudoAccount(std::set<SField const*> const& pseudoFieldFilter) const
{
auto const& fields = getPseudoAccountFields();
// Intentionally use defensive coding here because it's cheap and makes the
// semantics of true return value clean.
return sle_ && sle_->getType() == ltACCOUNT_ROOT &&
return this->sle_ && this->sle_->getType() == ltACCOUNT_ROOT &&
std::count_if(
fields.begin(), fields.end(), [this, &pseudoFieldFilter](SField const* sf) -> bool {
return sle_->isFieldPresent(*sf) &&
return this->sle_->isFieldPresent(*sf) &&
(pseudoFieldFilter.empty() || pseudoFieldFilter.contains(sf));
}) > 0;
}
@@ -235,18 +241,23 @@ createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const
return account;
}
template <typename ViewT>
[[nodiscard]] TER
AccountRoot::checkDestinationAndTag(bool hasDestinationTag) const
AccountRoot<ViewT>::checkDestinationAndTag(bool hasDestinationTag) const
{
if (sle_ == nullptr)
if (this->sle_ == nullptr)
return tecNO_DST;
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
if (sle_->isFlag(lsfRequireDestTag) && !hasDestinationTag)
if (this->sle_->isFlag(lsfRequireDestTag) && !hasDestinationTag)
return tecDST_TAG_NEEDED; // Cannot send without a tag
return tesSUCCESS;
}
// Explicit template instantiations
template class AccountRoot<ReadView>;
template class AccountRoot<ApplyView>;
} // namespace xrpl

View File

@@ -321,7 +321,7 @@ verifyDepositPreauth(
STTx const& tx,
ApplyView& view,
AccountID const& src,
AccountRoot const& dst,
ReadOnlyAccountRoot const& dst,
beast::Journal j)
{
// If depositPreauth is enabled, then an account that requires

View File

@@ -127,7 +127,7 @@ public:
//------------------------------------------------------------------------------
static error_code_i
acctMatchesPubKey(AccountRoot const& account, PublicKey const& publicKey)
acctMatchesPubKey(ReadOnlyAccountRoot const& account, PublicKey const& publicKey)
{
auto const publicKeyAcctID = calcAccountID(publicKey);
bool const isMasterKey = publicKeyAcctID == account.id();
@@ -457,7 +457,7 @@ transactionPreProcessImpl(
if (!verify && !tx_json.isMember(jss::Sequence))
return RPC::missing_field_error("tx_json.Sequence");
std::optional<AccountRoot> acctSrc;
std::optional<ReadOnlyAccountRoot> acctSrc;
if (verify)
acctSrc.emplace(srcAddressID, *app.openLedger().current());

View File

@@ -29,7 +29,7 @@ namespace xrpl {
* If the entry is not an account root, sets the 'Invalid' field to true.
*/
void
injectSLE(Json::Value& jv, AccountRoot const& account)
injectSLE(Json::Value& jv, ReadOnlyAccountRoot const& account)
{
jv = account->getJson(JsonOptions::none);
if (account->isFieldPresent(sfEmailHash))