Files
rippled/include/xrpl/protocol/STObject.h
Denis Angell e635557235 part 2
2026-05-14 05:56:04 +02:00

1718 lines
51 KiB
C++

/** @file
* Defines `STObject`, the heterogeneous field container that underlies every
* XRPL transaction, ledger entry, and inner object.
*
* `STObject` supports two operating modes: *free mode* (no schema, insertion
* order preserved) and *template mode* (schema enforced via `SOTemplate`,
* O(1) field lookup). Both `STTx` and `STLedgerEntry` are `final` subclasses.
*
* The proxy system (`ValueProxy`, `OptionalProxy`) provides type-safe,
* compile-time-checked field access via `operator[]` and `at()`, replacing
* the older `getFieldU32()`/`setFieldU32()` family for new code.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STCurrency.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/detail/STVar.h>
#include <boost/iterator/transform_iterator.hpp>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace xrpl {
class STArray;
/** Throw a `std::runtime_error` indicating a missing field.
*
* Used by the legacy typed-accessor family (`getFieldByValue`,
* `getFieldByConstRef`, `setFieldUsingSetValue`) when `peekAtPField` returns
* null. Not intended for direct use by callers outside `STObject`.
*
* @param field The field that could not be found.
* @throws std::runtime_error always.
*/
inline void
throwFieldNotFound(SField const& field)
{
Throw<std::runtime_error>("Field not found: " + field.getName());
}
/** Heterogeneous, field-keyed container for XRPL protocol objects.
*
* Stores an ordered sequence of `STBase`-derived fields, keyed by `SField`.
* Operates in one of two modes:
* - *Free mode* (`isFree() == true`): no schema; fields are stored in
* insertion order and any field may be added.
* - *Template mode* (`isFree() == false`): an `SOTemplate` constrains
* which fields are present, enforces `soeREQUIRED`/`soeOPTIONAL`/
* `soeDEFAULT` semantics, and enables O(1) field lookup.
*
* `STTx` and `STLedgerEntry` are the primary `final` subclasses.
* Field access is available via the modern proxy API (`operator[]`, `at()`)
* or the legacy typed-accessor family (`getFieldU32()`, etc.).
*
* @note `operator==` compares only wire-representable (`isBinary()`) fields.
*/
class STObject : public STBase, public CountedObject<STObject>
{
template <class T>
class Proxy;
template <class T>
class ValueProxy;
template <class T>
class OptionalProxy;
/** Functor used by `boost::transform_iterator` to project `STVar→STBase`. */
struct Transform
{
explicit Transform() = default;
using argument_type = detail::STVar;
using result_type = STBase;
STBase const&
operator()(detail::STVar const& e) const;
};
using list_type = std::vector<detail::STVar>;
list_type v_;
SOTemplate const* type_{};
public:
/** Forward iterator over the fields of this object as `STBase const&`. */
using iterator = boost::transform_iterator<Transform, STObject::list_type::const_iterator>;
~STObject() override = default;
STObject(STObject const&) = default;
/** Construct a templated `STObject` from a schema, field name, and
* an initializer callable.
*
* Delegates to `STObject(type, name)` then calls `f(*this)`, allowing
* fields to be populated inline at construction time.
*
* @param type The SOTemplate that defines the object's layout.
* @param name The SField identifying this object within its parent.
* @param f Callable `void(STObject&)` invoked after template init.
*/
template <typename F>
STObject(SOTemplate const& type, SField const& name, F&& f) : STObject(type, name)
{
f(*this);
}
STObject&
operator=(STObject const&) = default;
/** Move-construct, transferring field storage and template pointer. */
STObject(STObject&&);
/** Move-assign, transferring field storage and template pointer. */
STObject&
operator=(STObject&& other);
/** Construct a templated `STObject` pre-populated from a schema.
*
* Every slot in `type` is initialized: `soeREQUIRED` fields receive their
* type-default value; `soeOPTIONAL` and `soeDEFAULT` fields receive the
* `STI_NOTPRESENT` sentinel.
*
* @param type The SOTemplate that defines the object's layout.
* @param name The SField identifying this object within its parent.
*/
STObject(SOTemplate const& type, SField const& name);
/** Construct a templated `STObject` by deserializing from a byte stream.
*
* Reads fields in free mode from `sit`, then calls `applyTemplate(type)`
* to reorder and validate them against the schema.
*
* @param type The SOTemplate to enforce after deserialization.
* @param sit The byte stream to deserialize from.
* @param name The SField identifying this object within its parent.
* @throws FieldErr if a required field is missing or an unknown
* non-discardable field is present.
*/
STObject(SOTemplate const& type, SerialIter& sit, SField const& name);
/** Construct a free-mode `STObject` by deserializing from a byte stream.
*
* The `depth` parameter guards against stack exhaustion when parsing
* deeply nested structures from untrusted input; nesting beyond 10
* throws `std::runtime_error`.
*
* @param sit The byte stream to deserialize from.
* @param name The SField identifying this object within its parent.
* @param depth Current nesting depth (default 0); capped at 10.
* @throws std::runtime_error if depth exceeds 10 or data is malformed.
*/
STObject(SerialIter& sit, SField const& name, int depth = 0);
/** Construct from an rvalue `SerialIter`; delegates to the lvalue overload. */
STObject(SerialIter&& sit, SField const& name);
/** Construct a free-mode (schema-less) `STObject` with the given field name. */
explicit STObject(SField const& name);
/** Create a free-mode inner object, conditionally binding a schema template.
*
* Checks the ambient `getCurrentTransactionRules()` to determine whether
* the `fixInnerObjTemplate` or `fixInnerObjTemplate2` amendments are
* active, and applies the corresponding `SOTemplate` from
* `InnerObjectFormats` when they are. This amendment-gated behaviour
* preserves replay compatibility with historical ledger data serialized
* before schemas existed.
*
* @param name The SField identifying the type of inner object to create.
* @return A new `STObject`, bound to its schema when the active rules permit.
*/
static STObject
makeInnerObject(SField const& name);
/** Return an iterator to the first field in this object. */
[[nodiscard]] iterator
begin() const;
/** Return a past-the-end iterator for this object's fields. */
[[nodiscard]] iterator
end() const;
/** Return `true` when this object contains no fields. */
[[nodiscard]] bool
empty() const;
/** Reserve storage for at least `n` fields in the underlying vector. */
void
reserve(std::size_t n);
/** Validate and reorder fields against a schema after free-mode deserialization.
*
* Rebuilds the internal storage in template order. `soeREQUIRED` fields
* that are missing throw; `soeDEFAULT` fields whose serialized value equals
* the type's zero value are rejected (explicit defaults are forbidden);
* unknown non-discardable fields throw.
*
* @param type The SOTemplate to enforce.
* @throws FieldErr on required-field missing, explicit default value, or
* unknown non-discardable field.
*/
void
applyTemplate(SOTemplate const& type);
/** Look up and apply the schema registered for `sField` in `InnerObjectFormats`.
*
* No-op when no template is registered for the given field.
*
* @param sField The SField whose registered SOTemplate should be applied.
* @throws FieldErr (from `applyTemplate`) if the object does not conform.
*/
void
applyTemplateFromSField(SField const&);
/** Return `true` when no schema template is associated with this object. */
[[nodiscard]] bool
isFree() const;
/** Initialize this object from a template, pre-populating every slot.
*
* Clears existing fields and rebuilds in template order. `soeREQUIRED`
* fields receive their type-default value; all other fields receive the
* `STI_NOTPRESENT` sentinel.
*
* @param type The SOTemplate that defines the layout of this object.
*/
void
set(SOTemplate const&);
/** Deserialize fields from a byte stream into this object (free mode).
*
* Reads `(type, field)` ID pairs from `u`, constructs each child `STVar`
* at `depth+1`, and calls `applyTemplateFromSField()` on nested
* `STObject` children. Stops at an inner-object terminator byte.
*
* @param u The byte stream to read from.
* @param depth Current nesting depth; guards against deeply nested input.
* @return `true` if an inner-object terminator was consumed; `false`
* at top-level end-of-stream.
* @throws std::runtime_error on malformed data or duplicate fields.
*/
bool
set(SerialIter& u, int depth = 0);
/** Return `STI_OBJECT`. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return `true` when the objects have the same wire-representable fields.
*
* Only fields where `SField::isBinary()` is true participate in the
* comparison. Non-binary (JSON-only) fields are ignored.
*
* @param t The other serialized type to compare against.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` when this object holds no fields (empty storage). */
[[nodiscard]] bool
isDefault() const override;
/** Serialize all fields (including signing fields) into `s`. */
void
add(Serializer& s) const override;
/** Return a human-readable, field-by-field description of this object. */
[[nodiscard]] std::string
getFullText() const override;
/** Return a brief textual description of this object. */
[[nodiscard]] std::string
getText() const override;
// TODO(tom): options should be an enum.
/** Convert this object to a JSON value.
*
* @param options Controls formatting options (e.g., binary vs. human-readable).
*/
[[nodiscard]] json::Value getJson(JsonOptions = JsonOptions::Values::None) const override;
/** Serialize signing-eligible fields only into `s`.
*
* Excludes fields whose `SField::shouldInclude(false)` returns `false`
* (e.g., `sfTxnSignature`, `sfSigners`). Used to produce the payload
* that is hashed and signed or verified.
*
* @param s The serializer to append to.
*/
void
addWithoutSigningFields(Serializer& s) const;
/** Return a `Serializer` containing all fields (including signing fields).
*
* @note Produces a full copy of the serialized form; prefer
* `add(Serializer&)` when appending to an existing buffer.
*/
[[nodiscard]] Serializer
getSerializer() const;
/** Append a new field directly to the internal storage vector.
*
* Used during deserialization and by the proxy system when adding new
* fields to a free-mode object. Arguments are forwarded to
* `detail::STVar`'s constructor.
*
* @return The zero-based index of the newly added field.
*/
template <class... Args>
std::size_t
emplaceBack(Args&&... args);
/** Return the number of fields currently stored in this object. */
[[nodiscard]] int
getCount() const;
/** Set a flag bit in `sfFlags`, creating the field if absent.
*
* @param flag The flag bit(s) to set (OR-ed into existing flags).
* @return `true` if the flags field was changed.
*/
bool
setFlag(std::uint32_t);
/** Clear a flag bit in `sfFlags`.
*
* @param flag The flag bit(s) to clear.
* @return `true` if the flags field was changed.
*/
bool
clearFlag(std::uint32_t);
/** Return `true` when all bits in `flag` are set in `sfFlags`. */
[[nodiscard]] bool
isFlag(std::uint32_t) const;
/** Return the value of `sfFlags`, or 0 if the field is absent. */
[[nodiscard]] std::uint32_t
getFlags() const;
/** Compute a domain-separated hash of all fields (including signing fields).
*
* Prepends `prefix` before serializing all fields, then returns the
* `sha512Half` of the result.
*
* @param prefix The `HashPrefix` discriminator for this hash domain.
* @return The 256-bit hash.
*/
[[nodiscard]] uint256
getHash(HashPrefix prefix) const;
/** Compute a domain-separated hash of signing-eligible fields only.
*
* Equivalent to `getHash` but uses `addWithoutSigningFields` to exclude
* signature-carrying fields. Used by single-sig and multi-sig verification.
*
* @param prefix The `HashPrefix` discriminator for this hash domain.
* @return The 256-bit hash.
*/
[[nodiscard]] uint256
getSigningHash(HashPrefix prefix) const;
/** Return the field at `offset` by const reference; no bounds check. */
[[nodiscard]] STBase const&
peekAtIndex(int offset) const;
/** Return the field at `offset` by mutable reference; no bounds check. */
STBase&
getIndex(int offset);
/** Return a const pointer to the field at `offset`; no bounds check. */
[[nodiscard]] STBase const*
peekAtPIndex(int offset) const;
/** Return a mutable pointer to the field at `offset`; no bounds check. */
STBase*
getPIndex(int offset);
/** Return the storage index of `field`, or -1 if not present. */
[[nodiscard]] int
getFieldIndex(SField const& field) const;
/** Return the `SField` descriptor for the field at storage index `index`. */
[[nodiscard]] SField const&
getFieldSType(int index) const;
/** Return the field identified by `field` by const reference.
*
* @throws std::runtime_error if the field is not present.
*/
[[nodiscard]] STBase const&
peekAtField(SField const& field) const;
/** Return the field identified by `field` by mutable reference.
*
* @throws std::runtime_error if the field is not present.
*/
STBase&
getField(SField const& field);
/** Return a const pointer to the field identified by `field`, or `nullptr`.
*
* Does not create the field. Returns `nullptr` for absent optional fields
* in free mode; returns a pointer to the `STI_NOTPRESENT` sentinel in
* template mode.
*/
[[nodiscard]] STBase const*
peekAtPField(SField const& field) const;
/** Core field-lookup primitive, optionally creating the field.
*
* In template mode with `createOkay == false`, returns the stored slot
* pointer (which may be an `STI_NOTPRESENT` sentinel). With
* `createOkay == true`, promotes `STI_NOTPRESENT` sentinels and
* appends missing free-mode fields. Returns `nullptr` only when the
* field is absent in free mode and `createOkay` is false.
*
* @param field The field to locate.
* @param createOkay When `true`, create the field if absent.
* @return Pointer to the stored `STBase`, or `nullptr`.
*/
STBase*
getPField(SField const& field, bool createOkay = false);
/** @name Legacy typed field accessors
*
* These methods return field values by their native C++ type. They throw
* `std::runtime_error` if the field type does not match the expected type,
* and return a default-constructed value when an optional field is absent.
* Prefer the proxy API (`operator[]`, `at()`) for new code.
* @{
*/
[[nodiscard]] unsigned char
getFieldU8(SField const& field) const;
[[nodiscard]] std::uint16_t
getFieldU16(SField const& field) const;
[[nodiscard]] std::uint32_t
getFieldU32(SField const& field) const;
[[nodiscard]] std::uint64_t
getFieldU64(SField const& field) const;
[[nodiscard]] uint128
getFieldH128(SField const& field) const;
[[nodiscard]] uint160
getFieldH160(SField const& field) const;
[[nodiscard]] uint192
getFieldH192(SField const& field) const;
[[nodiscard]] uint256
getFieldH256(SField const& field) const;
[[nodiscard]] std::int32_t
getFieldI32(SField const& field) const;
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&
getFieldAmount(SField const& field) const;
[[nodiscard]] STPathSet const&
getFieldPathSet(SField const& field) const;
[[nodiscard]] STVector256 const&
getFieldV256(SField const& field) const;
/** Return the nested `STObject` for `field` by value.
*
* If the field is absent, returns a default-constructed `STObject`
* initialized with `field` as its name. Modifications to the returned
* copy do not propagate back; use `peekFieldObject()` for in-place access.
*/
[[nodiscard]] STObject
getFieldObject(SField const& field) const;
[[nodiscard]] STArray const&
getFieldArray(SField const& field) const;
[[nodiscard]] STCurrency const&
getFieldCurrency(SField const& field) const;
[[nodiscard]] STNumber const&
getFieldNumber(SField const& field) const;
/** @} */
/** Get the value of a field.
@param A TypedField built from an SField value representing the desired
object field. In typical use, the TypedField will be implicitly
constructed.
@return The value of the specified field.
@throws STObject::FieldErr if the field is not present.
*/
template <class T>
typename T::value_type
operator[](TypedField<T> const& f) const;
/** Get the value of a field as a std::optional
@param An OptionaledField built from an SField value representing the
desired object field. In typical use, the OptionaledField will be
constructed by using the ~ operator on an SField.
@return std::nullopt if the field is not present, else the value of
the specified field.
*/
template <class T>
std::optional<std::decay_t<typename T::value_type>>
operator[](OptionaledField<T> const& of) const;
/** Get a modifiable field value.
@param A TypedField built from an SField value representing the desired
object field. In typical use, the TypedField will be implicitly
constructed.
@return A modifiable reference to the value of the specified field.
@throws STObject::FieldErr if the field is not present.
*/
template <class T>
ValueProxy<T>
operator[](TypedField<T> const& f);
/** Return a modifiable field value as std::optional
@param An OptionaledField built from an SField value representing the
desired object field. In typical use, the OptionaledField will be
constructed by using the ~ operator on an SField.
@return Transparent proxy object to an `optional` holding a modifiable
reference to the value of the specified field. Returns
std::nullopt if the field is not present.
*/
template <class T>
OptionalProxy<T>
operator[](OptionaledField<T> const& of);
/** Get the value of a field.
@param A TypedField built from an SField value representing the desired
object field. In typical use, the TypedField will be implicitly
constructed.
@return The value of the specified field.
@throws STObject::FieldErr if the field is not present.
*/
template <class T>
[[nodiscard]] typename T::value_type
at(TypedField<T> const& f) const;
/** Get the value of a field as std::optional
@param An OptionaledField built from an SField value representing the
desired object field. In typical use, the OptionaledField will be
constructed by using the ~ operator on an SField.
@return std::nullopt if the field is not present, else the value of
the specified field.
*/
template <class T>
[[nodiscard]] std::optional<std::decay_t<typename T::value_type>>
at(OptionaledField<T> const& of) const;
/** Get a modifiable field value.
@param A TypedField built from an SField value representing the desired
object field. In typical use, the TypedField will be implicitly
constructed.
@return A modifiable reference to the value of the specified field.
@throws STObject::FieldErr if the field is not present.
*/
template <class T>
ValueProxy<T>
at(TypedField<T> const& f);
/** Return a modifiable field value as std::optional
@param An OptionaledField built from an SField value representing the
desired object field. In typical use, the OptionaledField will be
constructed by using the ~ operator on an SField.
@return Transparent proxy object to an `optional` holding a modifiable
reference to the value of the specified field. Returns
std::nullopt if the field is not present.
*/
template <class T>
OptionalProxy<T>
at(OptionaledField<T> const& of);
/** Replace or insert a field from a heap-allocated `STBase`.
*
* If a field with the same `SField` already exists, it is replaced.
*
* @param v The field to store; ownership is transferred.
*/
void
set(std::unique_ptr<STBase> v);
/** Replace or insert a field by move.
*
* If a field with the same `SField` already exists, it is replaced.
*
* @param v The field to move into this object.
*/
void
set(STBase&& v);
/** @name Legacy typed field mutators
*
* Set a named field to the given value. Throws `std::runtime_error` if
* the stored field has a different type than the value being set.
* Prefer the proxy API (`operator[]`, `at()`) for new code.
* @{
*/
void
setFieldU8(SField const& field, unsigned char);
void
setFieldU16(SField const& field, std::uint16_t);
void
setFieldU32(SField const& field, std::uint32_t);
void
setFieldU64(SField const& field, std::uint64_t);
void
setFieldH128(SField const& field, uint128 const&);
void
setFieldH192(SField const& field, uint192 const&);
void
setFieldH256(SField const& field, uint256 const&);
void
setFieldI32(SField const& field, std::int32_t);
void
setFieldVL(SField const& field, Blob const&);
void
setFieldVL(SField const& field, Slice const&);
void
setAccountID(SField const& field, AccountID const&);
void
setFieldAmount(SField const& field, STAmount const&);
void
setFieldIssue(SField const& field, STIssue const&);
void
setFieldCurrency(SField const& field, STCurrency const&);
void
setFieldNumber(SField const& field, STNumber const&);
void
setFieldPathSet(SField const& field, STPathSet const&);
void
setFieldV256(SField const& field, STVector256 const& v);
void
setFieldArray(SField const& field, STArray const& v);
void
setFieldObject(SField const& field, STObject const& v);
/** Set a 160-bit hash field from any `BaseUInt<160, Tag>` value.
*
* @tparam Tag The phantom tag type of the `BaseUInt` specialization.
* @param field The field to set.
* @param v The 160-bit value to store.
* @throws std::runtime_error if the stored field has a different type.
*/
template <class Tag>
void
setFieldH160(SField const& field, BaseUInt<160, Tag> const& v);
/** @} */
/** Return a mutable reference to the nested `STObject` for `field`.
*
* Unlike `getFieldObject()`, the returned reference is into internal
* storage; mutations propagate back to this object.
*
* @throws std::runtime_error if the field is absent or has the wrong type.
*/
STObject&
peekFieldObject(SField const& field);
/** Return a mutable reference to the nested `STArray` for `field`.
*
* The returned reference is into internal storage; mutations propagate
* back to this object.
*
* @throws std::runtime_error if the field is absent or has the wrong type.
*/
STArray&
peekFieldArray(SField const& field);
/** Return `true` when `field` is present and not an `STI_NOTPRESENT` sentinel. */
[[nodiscard]] bool
isFieldPresent(SField const& field) const;
/** Promote an `STI_NOTPRESENT` sentinel field to a live default value.
*
* In template mode, converts an optional/default slot from the sentinel
* state to a type-correct default value, making the field "present".
* In free mode, appends a new default-constructed field.
*
* @param field The field to make present.
* @return Pointer to the now-live field, or `nullptr` if not found.
*/
STBase*
makeFieldPresent(SField const& field);
/** Demote a live optional field back to the `STI_NOTPRESENT` sentinel.
*
* In template mode, replaces the field's value with the sentinel.
* In free mode, removes the field entry entirely.
* Only valid for `soeOPTIONAL` fields; called by the proxy system
* when assigning a `soeDEFAULT` field its zero value.
*
* @param field The field to make absent.
*/
void
makeFieldAbsent(SField const& field);
/** Remove the field identified by `field` unconditionally.
*
* @param field The field to remove.
* @return `true` if the field was found and removed.
*/
bool
delField(SField const& field);
/** Remove the field at storage index `index` unconditionally. */
void
delField(int index);
/** Return the `SOEStyle` (`soeREQUIRED`, `soeOPTIONAL`, `soeDEFAULT`)
* for `field` according to the associated template.
*
* @param field The field to query.
* @return The style, or `SoeInvalid` if the object is in free mode.
*/
[[nodiscard]] SOEStyle
getStyle(SField const& field) const;
/** Return `true` when any stored field compares equal to `entry`.
*
* Used to test membership in collections such as `STArray`.
*/
[[nodiscard]] bool
hasMatchingEntry(STBase const&) const;
/** Compare two `STObject` instances for equality.
*
* Only wire-representable (`isBinary()`) fields participate.
* This comparison is O(n²) by design.
*
* @note For fast same-template comparison, use `isEquivalent()` which
* short-circuits when both objects share the same `mType` pointer.
*/
bool
operator==(STObject const& o) const;
bool
operator!=(STObject const& o) const;
/** Exception thrown by the proxy and `at()` accessors on field errors.
*
* Raised when a required field is absent, a template constraint is
* violated, or an invalid field access is attempted on a free-mode object.
*/
class FieldErr;
private:
/** Selects which fields are included during serialization.
*
* The underlying `bool` values alias directly to `SField::shouldInclude(bool)`
* so they can be passed without translation:
* - `OmitSigningFields` (false) — exclude fields not intended for signing.
* - `WithAllFields` (true) — include every field.
*/
enum class WhichFields : bool {
// These values are carefully chosen to do the right thing if passed
// to SField::shouldInclude (bool)
OmitSigningFields = false,
WithAllFields = true
};
void
add(Serializer& s, WhichFields whichFields) const;
// Sort the entries in an STObject into the order that they will be
// serialized. Note: they are not sorted into pointer value order, they
// are sorted by SField::fieldCode.
static std::vector<STBase const*>
getSortedFields(STObject const& objToSort, WhichFields whichFields);
// Implementation for getting (most) fields that return by value.
//
// The remove_cv and remove_reference are necessitated by the STBitString
// types. Their value() returns by const ref. We return those types
// by value.
template <
typename T,
typename V = std::remove_cv_t<std::remove_reference_t<decltype(std::declval<T>().value())>>>
V
getFieldByValue(SField const& field) const;
// Implementations for getting (most) fields that return by const reference.
//
// If an absent optional field is deserialized we don't have anything
// obvious to return. So we insist on having the call provide an
// 'empty' value we return in that circumstance.
template <typename T, typename V>
V const&
getFieldByConstRef(SField const& field, V const& empty) const;
// Implementation for setting most fields with a setValue() method.
template <typename T, typename V>
void
setFieldUsingSetValue(SField const& field, V value);
// Implementation for setting fields using assignment
template <typename T>
void
setFieldUsingAssignment(SField const& field, T const& value);
// Implementation for peeking STObjects and STArrays
template <typename T>
T&
peekField(SField const& field);
STBase*
copy(std::size_t n, void* buf) const override;
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
};
//------------------------------------------------------------------------------
/** Common base for `ValueProxy<T>` and `OptionalProxy<T>`.
*
* Stores a back-pointer to the owning `STObject`, the `SOEStyle` of the
* field, and a typed descriptor pointer. Provides the read path
* (`value()`, `operator*`, `operator->`) and the write primitive `assign()`.
*
* `assign()` enforces `soeDEFAULT` canonicalization: assigning the zero
* value calls `makeFieldAbsent` rather than storing an explicit default,
* preserving canonical wire format.
*
* @tparam T The concrete `STBase`-derived type carrying the field's value.
*/
template <class T>
class STObject::Proxy
{
public:
using value_type = typename T::value_type;
/** Return the field's current value.
*
* For `soeDEFAULT` fields that are absent, returns a default-constructed
* `value_type`. Throws `FieldErr` for absent `soeOPTIONAL` or required
* fields, and when called on a free-mode object with no template.
*/
[[nodiscard]] value_type
value() const;
/** Dereference operator; equivalent to `value()`. */
value_type
operator*() const;
/// Do not use operator->() unless the field is required, or you've checked
/// that it's set.
T const*
operator->() const;
protected:
STObject* st_;
SOEStyle style_;
TypedField<T> const* f_;
Proxy(Proxy const&) = default;
Proxy(STObject* st, TypedField<T> const* f);
/** Locate the field via `dynamic_cast`; returns `nullptr` when absent. */
[[nodiscard]] T const*
find() const;
/** Write `u` into the field, applying `soeDEFAULT` canonicalization. */
template <class U>
void
assign(U&& u);
};
/** Satisfied by scalar arithmetic types, `Number`, and `STAmount`.
*
* Used to gate `ValueProxy::operator+=` and `operator-=` so they are only
* available for field types that support arithmetic operations.
*/
template <typename U>
concept IsArithmeticNumber =
std::is_arithmetic_v<U> || std::is_same_v<U, Number> || std::is_same_v<U, STAmount>;
/** Satisfied by phantom-typed `unit::ValueUnit<Unit, Value>` wrappers
* whose `Value` satisfies `IsArithmeticNumber`.
*/
template <
typename U,
typename Value = typename U::value_type,
typename Unit = typename U::unit_type>
concept IsArithmeticValueUnit = std::is_same_v<U, unit::ValueUnit<Unit, Value>> &&
IsArithmeticNumber<Value> && std::is_class_v<Unit>;
/** Satisfied by ST wrapper types (e.g., `STAmount`) that are not
* `ValueUnit` but whose `value_type` satisfies `IsArithmeticNumber`.
*/
template <typename U, typename Value = typename U::value_type>
concept IsArithmeticST = !IsArithmeticValueUnit<U> && IsArithmeticNumber<Value>;
/** Union of `IsArithmeticNumber`, `IsArithmeticST`, and `IsArithmeticValueUnit`. */
template <typename U>
concept IsArithmetic = IsArithmeticNumber<U> || IsArithmeticST<U> || IsArithmeticValueUnit<U>;
/** Satisfied when `T + U` compiles and the result is assignable back to `T`. */
template <class T, class U>
concept Addable = requires(T t, U u) { t = t + u; };
/** Satisfied when `T`'s `value_type` is arithmetic and supports addition with `U`. */
template <typename T, typename U>
concept IsArithmeticCompatible =
IsArithmetic<typename T::value_type> && Addable<typename T::value_type, U>;
/** Mutable proxy for a non-optional (`soeREQUIRED` or `soeDEFAULT`) field.
*
* Returned by the mutable overloads of `STObject::operator[]` and
* `STObject::at()`. Supports assignment, arithmetic `+=`/`-=` for
* compatible value types, and implicit conversion to `value_type` for
* transparent read-through.
*
* Copy-constructible but not copy-assignable; constructed only by `STObject`.
*
* @tparam T The concrete `STBase`-derived type carrying the field's value.
*/
template <class T>
class STObject::ValueProxy : public Proxy<T>
{
private:
using value_type = typename T::value_type;
public:
ValueProxy(ValueProxy const&) = default;
ValueProxy&
operator=(ValueProxy const&) = delete;
/** Assign `u` to the field, delegating to `Proxy::assign()`. */
template <class U>
std::enable_if_t<std::is_assignable_v<T, U>, ValueProxy&>
operator=(U&& u);
/** Add `u` to the current field value and write back. */
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator+=(U const& u);
/** Subtract `u` from the current field value and write back. */
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator-=(U const& u);
/** Implicit conversion to `value_type` for transparent read-through. */
operator value_type() const;
template <typename U>
friend bool
operator==(U const& lhs, STObject::ValueProxy<T> const& rhs)
{
return rhs.value() == lhs;
}
private:
friend class STObject;
ValueProxy(STObject* st, TypedField<T> const* f);
};
/** Mutable proxy for an optional (`soeOPTIONAL`) field.
*
* Returned by the mutable overloads of `STObject::operator[]` and
* `STObject::at()` when called with an `OptionaledField<T>`. Supports
* assignment from a value, `std::optional`, or `std::nullopt` (to remove
* the field). Implicit conversion to `optional_type` enables use in
* standard optional contexts.
*
* Assigning `std::nullopt` to a `soeREQUIRED` or `soeDEFAULT` field throws
* `FieldErr`; required fields cannot be removed and default-value fields are
* semantically always present.
*
* Copy-constructible but not copy-assignable; constructed only by `STObject`.
*
* @tparam T The concrete `STBase`-derived type carrying the field's value.
*/
template <class T>
class STObject::OptionalProxy : public Proxy<T>
{
private:
using value_type = typename T::value_type;
using optional_type = std::optional<std::decay_t<value_type>>;
public:
OptionalProxy(OptionalProxy const&) = default;
OptionalProxy&
operator=(OptionalProxy const&) = delete;
/** Returns `true` if the field is set.
Fields with soeDEFAULT and set to the
default value will return `true`
*/
explicit
operator bool() const noexcept;
/** Implicit conversion to `std::optional<value_type>`. */
operator optional_type() const;
/** Explicit conversion to std::optional */
optional_type
operator~() const;
friend bool
operator==(OptionalProxy const& lhs, std::nullopt_t) noexcept
{
return !lhs.engaged();
}
friend bool
operator==(std::nullopt_t, OptionalProxy const& rhs) noexcept
{
return rhs == std::nullopt;
}
friend bool
operator==(OptionalProxy const& lhs, optional_type const& rhs) noexcept
{
if (!lhs.engaged())
return !rhs;
if (!rhs)
return false;
return *lhs == *rhs;
}
friend bool
operator==(optional_type const& lhs, OptionalProxy const& rhs) noexcept
{
return rhs == lhs;
}
friend bool
operator==(OptionalProxy const& lhs, OptionalProxy const& rhs) noexcept
{
if (lhs.engaged() != rhs.engaged())
return false;
return !lhs.engaged() || *lhs == *rhs;
}
friend bool
operator!=(OptionalProxy const& lhs, std::nullopt_t) noexcept
{
return !(lhs == std::nullopt);
}
friend bool
operator!=(std::nullopt_t, OptionalProxy const& rhs) noexcept
{
return !(rhs == std::nullopt);
}
friend bool
operator!=(OptionalProxy const& lhs, optional_type const& rhs) noexcept
{
return !(lhs == rhs);
}
friend bool
operator!=(optional_type const& lhs, OptionalProxy const& rhs) noexcept
{
return !(lhs == rhs);
}
friend bool
operator!=(OptionalProxy const& lhs, OptionalProxy const& rhs) noexcept
{
return !(lhs == rhs);
}
/** Return the field's value if present, otherwise `val`. */
[[nodiscard]] value_type
valueOr(value_type val) const;
/** Remove the field (make it absent).
*
* @throws FieldErr if the field is `soeREQUIRED` or `soeDEFAULT`.
*/
OptionalProxy&
operator=(std::nullopt_t const&);
/** Assign from an rvalue `std::optional`; removes the field if `nullopt`. */
OptionalProxy&
operator=(optional_type&& v); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
/** Assign from a const `std::optional`; removes the field if `nullopt`. */
OptionalProxy&
operator=(optional_type const& v);
/** Assign a value directly to the field. */
template <class U>
std::enable_if_t<std::is_assignable_v<T, U>, OptionalProxy&>
operator=(U&& u);
private:
friend class STObject;
OptionalProxy(STObject* st, TypedField<T> const* f);
/** Return `true` when the field is present (not the `STI_NOTPRESENT` sentinel). */
[[nodiscard]] bool
engaged() const noexcept;
/** Remove the field, enforcing `soeREQUIRED`/`soeDEFAULT` constraints. */
void
disengage();
/** Return the current value as `optional_type`, or `nullopt` if absent. */
[[nodiscard]] optional_type
optionalValue() const;
};
class STObject::FieldErr : public std::runtime_error
{
using std::runtime_error::runtime_error;
};
template <class T>
STObject::Proxy<T>::Proxy(STObject* st, TypedField<T> const* f) : st_(st), f_(f)
{
if (st_->type_ != nullptr)
{
// STObject has associated template
if (!st_->peekAtPField(*f_))
Throw<STObject::FieldErr>("Template field error '" + this->f_->getName() + "'");
style_ = st_->type_->style(*f_);
}
else
{
style_ = SoeInvalid;
}
}
template <class T>
auto
STObject::Proxy<T>::value() const -> value_type
{
auto const t = find();
if (t)
return t->value();
if (style_ == SoeInvalid)
{
Throw<STObject::FieldErr>("Value requested from invalid STObject.");
}
if (style_ != SoeDefault)
{
Throw<STObject::FieldErr>("Missing field '" + this->f_->getName() + "'");
}
return value_type{};
}
template <class T>
auto
STObject::Proxy<T>::operator*() const -> value_type
{
return this->value();
}
/// Do not use operator->() unless the field is required, or you've checked that
/// it's set.
template <class T>
T const*
STObject::Proxy<T>::operator->() const
{
return this->find();
}
template <class T>
inline T const*
STObject::Proxy<T>::find() const
{
return dynamic_cast<T const*>(st_->peekAtPField(*f_));
}
template <class T>
template <class U>
void
STObject::Proxy<T>::assign(U&& u)
{
if (style_ == SoeDefault && u == value_type{})
{
st_->makeFieldAbsent(*f_);
return;
}
T* t = nullptr;
if (style_ == SoeInvalid)
{
t = dynamic_cast<T*>(st_->getPField(*f_, true));
}
else
{
t = dynamic_cast<T*>(st_->makeFieldPresent(*f_));
}
XRPL_ASSERT(t, "xrpl::STObject::Proxy::assign : type cast succeeded");
*t = std::forward<U>(u);
}
//------------------------------------------------------------------------------
template <class T>
template <class U>
std::enable_if_t<std::is_assignable_v<T, U>, STObject::ValueProxy<T>&>
STObject::ValueProxy<T>::operator=(U&& u)
{
this->assign(std::forward<U>(u));
return *this;
}
template <typename T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator+=(U const& u)
{
this->assign(this->value() + u);
return *this;
}
template <class T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator-=(U const& u)
{
this->assign(this->value() - u);
return *this;
}
template <class T>
STObject::ValueProxy<T>::
operator value_type() const
{
return this->value();
}
template <class T>
STObject::ValueProxy<T>::ValueProxy(STObject* st, TypedField<T> const* f) : Proxy<T>(st, f)
{
}
//------------------------------------------------------------------------------
template <class T>
STObject::OptionalProxy<T>::
operator bool() const noexcept
{
return engaged();
}
template <class T>
STObject::OptionalProxy<T>::
operator typename STObject::OptionalProxy<T>::optional_type() const
{
return optionalValue();
}
template <class T>
typename STObject::OptionalProxy<T>::optional_type
STObject::OptionalProxy<T>::operator~() const
{
return optionalValue();
}
template <class T>
auto
STObject::OptionalProxy<T>::operator=(std::nullopt_t const&) -> OptionalProxy&
{
disengage();
return *this;
}
template <class T>
auto
STObject::OptionalProxy<T>::operator=(
optional_type&& v) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
-> OptionalProxy&
{
if (v)
{
this->assign(std::move(*v));
}
else
{
disengage();
}
return *this;
}
template <class T>
auto
STObject::OptionalProxy<T>::operator=(optional_type const& v) -> OptionalProxy&
{
if (v)
{
this->assign(*v);
}
else
{
disengage();
}
return *this;
}
template <class T>
template <class U>
std::enable_if_t<std::is_assignable_v<T, U>, STObject::OptionalProxy<T>&>
STObject::OptionalProxy<T>::operator=(U&& u)
{
this->assign(std::forward<U>(u));
return *this;
}
template <class T>
STObject::OptionalProxy<T>::OptionalProxy(STObject* st, TypedField<T> const* f) : Proxy<T>(st, f)
{
}
template <class T>
bool
STObject::OptionalProxy<T>::engaged() const noexcept
{
return this->style_ == SoeDefault || this->find() != nullptr;
}
template <class T>
void
STObject::OptionalProxy<T>::disengage()
{
if (this->style_ == SoeRequired || this->style_ == SoeDefault)
Throw<STObject::FieldErr>("Template field error '" + this->f_->getName() + "'");
if (this->style_ == SoeInvalid)
{
this->st_->delField(*this->f_);
}
else
{
this->st_->makeFieldAbsent(*this->f_);
}
}
template <class T>
auto
STObject::OptionalProxy<T>::optionalValue() const -> optional_type
{
if (!engaged())
return std::nullopt;
return this->value();
}
template <class T>
typename STObject::OptionalProxy<T>::value_type
STObject::OptionalProxy<T>::valueOr(value_type val) const
{
return engaged() ? this->value() : val;
}
//------------------------------------------------------------------------------
inline STBase const&
STObject::Transform::operator()(detail::STVar const& e) const
{
return e.get();
}
//------------------------------------------------------------------------------
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
inline STObject::STObject(SerialIter&& sit, SField const& name) : STObject(sit, name)
{
}
inline STObject::iterator
STObject::begin() const
{
return iterator(v_.begin());
}
inline STObject::iterator
STObject::end() const
{
return iterator(v_.end());
}
inline bool
STObject::empty() const
{
return v_.empty();
}
inline void
STObject::reserve(std::size_t n)
{
v_.reserve(n);
}
inline bool
STObject::isFree() const
{
return type_ == nullptr;
}
inline void
STObject::addWithoutSigningFields(Serializer& s) const
{
add(s, WhichFields::OmitSigningFields);
}
// VFALCO NOTE does this return an expensive copy of an object with a
// dynamic buffer?
// VFALCO TODO Remove this function and fix the few callers.
inline Serializer
STObject::getSerializer() const
{
Serializer s;
add(s, WhichFields::WithAllFields);
return s;
}
template <class... Args>
inline std::size_t
STObject::emplaceBack(Args&&... args)
{
v_.emplace_back(std::forward<Args>(args)...);
return v_.size() - 1;
}
inline int
STObject::getCount() const
{
return v_.size();
}
inline STBase const&
STObject::peekAtIndex(int offset) const
{
return v_[offset].get();
}
inline STBase&
STObject::getIndex(int offset)
{
return v_[offset].get();
}
inline STBase const*
STObject::peekAtPIndex(int offset) const
{
return &v_[offset].get();
}
inline STBase*
STObject::getPIndex(int offset)
{
return &v_[offset].get();
}
template <class T>
typename T::value_type
STObject::operator[](TypedField<T> const& f) const
{
return at(f);
}
template <class T>
std::optional<std::decay_t<typename T::value_type>>
STObject::operator[](OptionaledField<T> const& of) const
{
return at(of);
}
template <class T>
inline auto
STObject::operator[](TypedField<T> const& f) -> ValueProxy<T>
{
return at(f);
}
template <class T>
inline auto
STObject::operator[](OptionaledField<T> const& of) -> OptionalProxy<T>
{
return at(of);
}
template <class T>
[[nodiscard]] typename T::value_type
STObject::at(TypedField<T> const& f) const
{
auto const b = peekAtPField(f);
if (!b)
{
// This is a free object (no constraints)
// with no template
Throw<STObject::FieldErr>("Missing field: " + f.getName());
}
if (auto const u = dynamic_cast<T const*>(b))
return u->value();
XRPL_ASSERT(type_, "xrpl::STObject::at(TypedField auto) : field template non-null");
XRPL_ASSERT(
b->getSType() == STI_NOTPRESENT, "xrpl::STObject::at(TypedField auto) : type not present");
if (type_->style(f) == SoeOptional)
Throw<STObject::FieldErr>("Missing optional field: " + f.getName());
XRPL_ASSERT(
type_->style(f) == SoeDefault,
"xrpl::STObject::at(TypedField auto) : template style is default");
// Used to help handle the case where value_type is a const reference,
// otherwise we would return the address of a temporary.
static std::decay_t<typename T::value_type> const kDV{};
return kDV;
}
template <class T>
[[nodiscard]] std::optional<std::decay_t<typename T::value_type>>
STObject::at(OptionaledField<T> const& of) const
{
auto const b = peekAtPField(*of.f);
if (!b)
return std::nullopt;
auto const u = dynamic_cast<T const*>(b);
if (!u)
{
XRPL_ASSERT(
type_,
"xrpl::STObject::at(OptionaledField auto) : field template "
"non-null");
XRPL_ASSERT(
b->getSType() == STI_NOTPRESENT,
"xrpl::STObject::at(OptionaledField auto) : type not present");
if (type_->style(*of.f) == SoeOptional)
return std::nullopt;
XRPL_ASSERT(
type_->style(*of.f) == SoeDefault,
"xrpl::STObject::at(OptionaledField auto) : template style is "
"default");
return typename T::value_type{};
}
return u->value();
}
template <class T>
inline auto
STObject::at(TypedField<T> const& f) -> ValueProxy<T>
{
return ValueProxy<T>(this, &f);
}
template <class T>
inline auto
STObject::at(OptionaledField<T> const& of) -> OptionalProxy<T>
{
return OptionalProxy<T>(this, of.f);
}
template <class Tag>
void
STObject::setFieldH160(SField const& field, BaseUInt<160, Tag> const& v)
{
STBase* rf = getPField(field, true);
if (!rf)
throwFieldNotFound(field);
if (rf->getSType() == STI_NOTPRESENT)
rf = makeFieldPresent(field);
using Bits = STBitString<160>;
if (auto cf = dynamic_cast<Bits*>(rf))
{
cf->setValue(v);
}
else
{
Throw<std::runtime_error>("Wrong field type");
}
}
inline bool
STObject::operator!=(STObject const& o) const
{
return !(*this == o);
}
template <typename T, typename V>
V
STObject::getFieldByValue(SField const& field) const
{
STBase const* rf = peekAtPField(field);
if (!rf)
throwFieldNotFound(field);
SerializedTypeID const id = rf->getSType();
if (id == STI_NOTPRESENT)
return V(); // optional field not present
T const* cf = dynamic_cast<T const*>(rf);
if (!cf)
Throw<std::runtime_error>("Wrong field type");
return cf->value();
}
// Implementations for getting (most) fields that return by const reference.
//
// If an absent optional field is deserialized we don't have anything
// obvious to return. So we insist on having the call provide an
// 'empty' value we return in that circumstance.
template <typename T, typename V>
V const&
STObject::getFieldByConstRef(SField const& field, V const& empty) const
{
STBase const* rf = peekAtPField(field);
if (!rf)
throwFieldNotFound(field);
SerializedTypeID const id = rf->getSType();
if (id == STI_NOTPRESENT)
{
// NOLINTNEXTLINE(bugprone-return-const-ref-from-parameter)
return empty; // optional field not present
}
T const* cf = dynamic_cast<T const*>(rf);
if (!cf)
Throw<std::runtime_error>("Wrong field type");
return *cf;
}
// Implementation for setting most fields with a setValue() method.
template <typename T, typename V>
void
STObject::setFieldUsingSetValue(SField const& field, V value)
{
static_assert(!std::is_lvalue_reference_v<V>, "");
STBase* rf = getPField(field, true);
if (!rf)
throwFieldNotFound(field);
if (rf->getSType() == STI_NOTPRESENT)
rf = makeFieldPresent(field);
T* cf = dynamic_cast<T*>(rf);
if (!cf)
Throw<std::runtime_error>("Wrong field type");
cf->setValue(std::move(value));
}
// Implementation for setting fields using assignment
template <typename T>
void
STObject::setFieldUsingAssignment(SField const& field, T const& value)
{
STBase* rf = getPField(field, true);
if (!rf)
throwFieldNotFound(field);
if (rf->getSType() == STI_NOTPRESENT)
rf = makeFieldPresent(field);
T* cf = dynamic_cast<T*>(rf);
if (!cf)
Throw<std::runtime_error>("Wrong field type");
(*cf) = value;
}
// Implementation for peeking STObjects and STArrays
template <typename T>
T&
STObject::peekField(SField const& field)
{
STBase* rf = getPField(field, true);
if (!rf)
throwFieldNotFound(field);
if (rf->getSType() == STI_NOTPRESENT)
rf = makeFieldPresent(field);
T* cf = dynamic_cast<T*>(rf);
if (!cf)
Throw<std::runtime_error>("Wrong field type");
return *cf;
}
} // namespace xrpl