This commit is contained in:
Denis Angell
2026-05-14 05:56:04 +02:00
parent d8febb71bd
commit e635557235
252 changed files with 38531 additions and 6523 deletions

6
.gitignore vendored
View File

@@ -19,6 +19,12 @@ Release/
/.build/
/.venv/
/build/
/build-base/
/doc-coverage.info
/base-doc-coverage.info
/doc-coverage-report.md
/doc-review-report.md
/doc-review-comments.json
/db/
/out.txt
/Testing/

View File

@@ -1,3 +1,14 @@
/** @file
* Declares `ApplyViewBase`, the abstract concrete base class shared by all
* buffered mutable ledger views used during transaction application.
*
* `ApplyViewBase` lives in `xrpl::detail` to signal that it is internal
* infrastructure; transaction processing code works with `ApplyView` or
* `ApplyViewImpl` references. The three concrete subclasses —
* `ApplyViewImpl`, `Sandbox`, and `PaymentSandbox` — are the only types
* that need to reach into this layer directly.
*/
#pragma once
#include <xrpl/ledger/ApplyView.h>
@@ -119,6 +130,12 @@ public:
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
slesEnd() const override;
/** Return an iterator to the first SLE whose key is not less than `key`,
* drawn from the base snapshot only.
*
* @param key The lower-bound key for the search.
* @return An iterator into the base SLE map at or after `key`.
*/
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
slesUpperBound(uint256 const& key) const override;
/** @} */
@@ -131,9 +148,21 @@ public:
[[nodiscard]] std::unique_ptr<TxsType::iter_base>
txsEnd() const override;
/** Test whether a transaction exists in the base snapshot's tx-map.
*
* @param key The transaction ID to look up.
* @return `true` if the transaction is present in the base ledger's
* transaction map.
*/
[[nodiscard]] bool
txExists(key_type const& key) const override;
/** Read a transaction and its metadata from the base snapshot's tx-map.
*
* @param key The transaction ID to retrieve.
* @return A pair of `(STTx, STObject metadata)` for the transaction,
* or `{nullptr, nullptr}` if not found.
*/
[[nodiscard]] tx_type
txRead(key_type const& key) const override;
/** @} */

View File

@@ -11,27 +11,73 @@
namespace xrpl::detail {
// Helper class that buffers raw modifications
/** In-memory write buffer that accumulates SLE mutations before flushing them
* to a backing `RawView`.
*
* Every mutable ledger view (`OpenView`, and indirectly `ApplyStateTable`)
* embeds a `RawStateTable` as its delta accumulator. The three mutation
* methods — `erase`, `insert`, and `replace` — apply a state-machine
* collapse so the map stays minimal: insert-then-erase cancels out entirely;
* erase-then-insert upgrades to replace; and illegal sequences (double-erase,
* double-insert) throw `std::logic_error`. `read`, `exists`, and `succ`
* overlay the pending delta transparently onto the supplied base `ReadView`,
* so callers always see a coherent merged state. Once a transaction succeeds,
* `apply()` flushes the buffer to the target `RawView` in a single pass.
*
* The `items_` map uses a `boost::container::pmr::monotonic_buffer_resource`
* with a 256 KB initial arena for O(1) amortised allocation during the burst
* of mutations that constitute a single transaction round. Because the
* resource cannot be shared or assigned, copy construction allocates a fresh
* resource and deep-copies the map; move construction transfers the
* `unique_ptr` directly. Both assignment operators are deleted.
*
* XRP fee destruction is tracked separately in `dropsDestroyed_` and
* replayed as a single `rawDestroyXRP` call during `apply()`.
*
* @note This class is an internal implementation detail of `OpenView`.
* Transaction logic should not interact with it directly; use the
* `RawView` interface instead.
* @see OpenView, RawView
*/
class RawStateTable
{
public:
using key_type = ReadView::key_type;
// Initial size for the monotonic_buffer_resource used for allocations
// The size was chosen from the old `qalloc` code (which this replaces).
// It is unclear how the size initially chosen in qalloc.
/** Initial arena size for the PMR monotonic buffer resource.
*
* Inherited from the legacy `qalloc` scheme this replaced. The 256 KB
* budget covers the typical per-transaction working set without triggering
* heap growth for the common case.
*/
static constexpr size_t kINITIAL_BUFFER_SIZE = kilobytes(256);
/** Construct an empty table with a fresh 256 KB monotonic arena. */
RawStateTable()
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
kINITIAL_BUFFER_SIZE)}
, items_{monotonic_resource_.get()} {};
/** Copy-construct by allocating a fresh monotonic arena and copying items.
*
* The SLE `shared_ptr` values in `items_` are shared with the source —
* not deep-copied — which is safe because SLEs are immutable once
* published. `dropsDestroyed_` is copied verbatim.
*
* @param rhs The source table to copy.
*/
RawStateTable(RawStateTable const& rhs)
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
kINITIAL_BUFFER_SIZE)}
, items_{rhs.items_, monotonic_resource_.get()}
, dropsDestroyed_{rhs.dropsDestroyed_} {};
/** Move-construct by transferring the monotonic resource and items map.
*
* After the move, the source table is left in a valid but empty state.
* The `unique_ptr` transfer preserves the stable address that `items_`'
* `polymorphic_allocator` holds.
*/
RawStateTable(RawStateTable&&) = default;
RawStateTable&
@@ -39,48 +85,166 @@ public:
RawStateTable&
operator=(RawStateTable const&) = delete;
/** Flush all buffered mutations to a backing `RawView`.
*
* First calls `to.rawDestroyXRP(dropsDestroyed_)` to replay accumulated
* fee burns, then iterates `items_` and dispatches each pending action
* to the corresponding `rawErase`, `rawInsert`, or `rawReplace` method.
* The table is not cleared after apply; this object should be discarded
* or destroyed once flushed.
*
* @param to The target `RawView` that receives all buffered mutations.
*/
void
apply(RawView& to) const;
/** Test whether an SLE exists, overlaying the pending delta onto `base`.
*
* Checks the pending buffer first: a pending erase returns `false`; a
* pending insert or replace returns `true` only if `k.check()` passes
* (type-tag validation). Falls through to `base.exists(k)` when the key
* has no pending action.
*
* @param base The underlying read-only ledger state.
* @param k The keylet specifying key and expected SLE type.
* @return `true` if the entry exists and its type satisfies `k.check()`.
*/
[[nodiscard]] bool
exists(ReadView const& base, Keylet const& k) const;
/** Find the smallest key strictly greater than `key` in the merged state.
*
* Runs two parallel searches: (1) walks `base.succ()` repeatedly,
* skipping any base key that has a pending `Action::Erase`; (2) scans
* `items_` forward from `key` for the first non-erase entry. Returns
* the lower of the two candidates. If `last` is given and the result is
* `>= last`, returns `std::nullopt` (half-open range semantics).
*
* @param base The underlying read-only ledger state.
* @param key Exclusive lower bound; the search begins strictly after this.
* @param last Optional exclusive upper bound; `std::nullopt` means unbounded.
* @return The next existing key, or `std::nullopt` if none is in range.
*/
[[nodiscard]] std::optional<key_type>
succ(ReadView const& base, key_type const& key, std::optional<key_type> const& last) const;
/** Stage an SLE deletion, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Erase`.
* - `Insert` → removes the entry entirely (net-zero; base is unaffected).
* - `Replace` → downgrades to `Action::Erase`.
* - `Erase` → `LogicError` (double-delete).
*
* @param sle The ledger entry to stage for deletion; key is taken from the SLE.
* @throws std::logic_error if the key already has a pending erase.
*/
void
erase(std::shared_ptr<SLE> const& sle);
/** Stage an SLE creation, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Insert`.
* - `Erase` → upgrades to `Action::Replace` (delete-then-recreate in
* the same transaction batch).
* - `Insert` → `LogicError` (duplicate insert).
* - `Replace` → `LogicError` (key already present in the delta).
*
* @param sle The new ledger entry to stage; key is taken from the SLE.
* @throws std::logic_error if the key is already pending insert or replace.
*/
void
insert(std::shared_ptr<SLE> const& sle);
/** Stage an SLE field update, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Replace`.
* - `Insert` → updates the stored SLE pointer; preserves `Insert`
* because from the base's perspective the key is still being created.
* - `Replace` → updates the stored SLE pointer.
* - `Erase` → `LogicError` (cannot replace a deleted key).
*
* @param sle The updated ledger entry to stage; key is taken from the SLE.
* @throws std::logic_error if the key has a pending erase.
*/
void
replace(std::shared_ptr<SLE> const& sle);
/** Read an SLE, overlaying the pending delta onto `base`.
*
* Checks the buffer first: a pending erase returns `nullptr`; a pending
* insert or replace returns the buffered SLE if `k.check()` passes
* (guards against type mismatches at the same key). Falls through to
* `base.read(k)` when the key has no pending action.
*
* @param base The underlying read-only ledger state.
* @param k The keylet specifying key and expected SLE type.
* @return The SLE if it exists and the type matches, otherwise `nullptr`.
*/
[[nodiscard]] std::shared_ptr<SLE const>
read(ReadView const& base, Keylet const& k) const;
/** Accumulate XRP drops to destroy at `apply()` time.
*
* Drops are not forwarded individually; they accumulate in
* `dropsDestroyed_` and are replayed as a single `rawDestroyXRP` call in
* `apply()`, keeping fee-burn accounting atomic with the rest of the flush.
*
* @param fee The quantity of XRP drops to add to the accumulated burn total.
*/
void
destroyXRP(XRPAmount const& fee);
/** Return a begin iterator for the merged SLE range over `base` and the delta.
*
* The returned iterator implements the two-pointer merge defined by
* `SlesIterImpl`: pending inserts appear in sorted position, pending
* erases are hidden, and pending replaces shadow the base entry.
*
* @param base The underlying read-only ledger state to merge with.
* @return A heap-allocated `iter_base` positioned at the first merged SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesBegin(ReadView const& base) const;
/** Return an end sentinel for the merged SLE range over `base` and the delta.
*
* @param base The underlying read-only ledger state to merge with.
* @return A heap-allocated `iter_base` positioned past the last merged SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesEnd(ReadView const& base) const;
/** Return an iterator to the first merged SLE with key strictly greater
* than `key`.
*
* @param base The underlying read-only ledger state to merge with.
* @param key Exclusive lower bound for the search.
* @return A heap-allocated `iter_base` positioned at the first qualifying SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesUpperBound(ReadView const& base, uint256 const& key) const;
private:
/** Pending mutation kind for an entry in `items_`. */
enum class Action {
Erase,
Insert,
Replace,
Erase, /**< Entry is scheduled for deletion. */
Insert, /**< Entry is being created; does not yet exist in the base. */
Replace, /**< Entry exists in the base and has been modified. */
};
/** Private iterator class that merges base-view SLEs with the pending
* delta; defined in the `.cpp`. */
class SlesIterImpl;
/** Pairs a pending `Action` with the SLE it acts on.
*
* Stored as the mapped value in `items_`. The SLE pointer is always
* non-null; for `Erase` it is the last version written before the
* deletion was staged (used by `RawView::rawErase`).
*/
struct SleAction
{
Action action;
@@ -99,11 +263,17 @@ private:
SleAction,
std::less<key_type>,
boost::container::pmr::polymorphic_allocator<std::pair<key_type const, SleAction>>>;
// monotonic_resource_ must outlive `items_`. Make a pointer so it may be
// easily moved.
std::unique_ptr<boost::container::pmr::monotonic_buffer_resource> monotonic_resource_;
/** Ordered map from ledger key to pending mutation; backed by the
* monotonic arena for O(1) amortised node allocation. */
items_t items_;
/** Accumulated XRP drops burned by fees; replayed as one `rawDestroyXRP`
* call during `apply()`. */
XRPAmount dropsDestroyed_{0};
};

View File

@@ -1,3 +1,13 @@
/** @file
* Type-erased forward-iterator infrastructure for `ReadView` traversal.
*
* Defines `ReadViewFwdIter` (the abstract iterator interface) and
* `ReadViewFwdRange` (the STL-compatible range wrapper) that together let
* any `ReadView` subclass expose its state and transaction maps through a
* single, stable iterator type. Callers interact indirectly via
* `ReadView::sles` and `ReadView::txs`; this header is internal plumbing.
*/
#pragma once
#include <cstddef>
@@ -10,8 +20,18 @@ class ReadView;
namespace detail {
// A type-erased ForwardIterator
//
/** Abstract base defining the four primitive operations of a type-erased forward iterator.
*
* Each concrete `ReadView` implementation provides a private subclass of
* this template and hands heap-allocated instances to `ReadViewFwdRange::Iterator`
* via the factory methods `slesBegin()`, `slesEnd()`, `slesUpperBound()`,
* `txsBegin()`, and `txsEnd()` on `ReadView`. Callers never interact with
* this class directly.
*
* @tparam ValueType The element type yielded by the iterator —
* `std::shared_ptr<SLE const>` for state-map iteration or
* `ReadView::tx_type` for transaction-map iteration.
*/
template <class ValueType>
class ReadViewFwdIter
{
@@ -27,21 +47,57 @@ public:
virtual ~ReadViewFwdIter() = default;
/** Returns a heap-allocated deep copy of this iterator.
*
* Provides value-semantics copy for the owning `unique_ptr` wrapper.
* Each concrete subclass must return a new instance of itself in the
* same position.
*
* @return A `unique_ptr` to a fresh copy of this iterator instance.
*/
[[nodiscard]] virtual std::unique_ptr<ReadViewFwdIter>
copy() const = 0;
/** Returns `true` if this iterator denotes the same position as @p impl.
*
* Both iterators must be over the same underlying view; mixing iterators
* from different views produces undefined behavior.
*
* @param impl The other iterator to compare against.
* @return `true` when both iterators point to the same element (or both
* are end sentinels).
*/
[[nodiscard]] virtual bool
equal(ReadViewFwdIter const& impl) const = 0;
/** Advances this iterator to the next element in the sequence. */
virtual void
increment() = 0;
/** Returns the element at the current iterator position.
*
* @return The current `ValueType` value. The result is cached by the
* wrapping `Iterator` so repeated dereferences are inexpensive.
* @throw May throw if the underlying view operation fails.
*/
[[nodiscard]] virtual value_type
dereference() const = 0;
};
// A range using type-erased ForwardIterator
//
/** STL-compatible forward range backed by a type-erased iterator.
*
* Wraps a `ReadViewFwdIter<ValueType>` behind a regular value-type iterator
* so that callers can write range-for loops over any `ReadView` subclass
* without knowing the concrete iterator type. Virtual dispatch is hidden
* inside the `impl_` pointer; the public `Iterator` API is fully inlined.
*
* `ReadView::SlesType` and `ReadView::TxsType` inherit from this template;
* application code should use those types rather than instantiating
* `ReadViewFwdRange` directly.
*
* @tparam ValueType The element type — must be noexcept-move-constructible
* so that `Iterator` move operations are noexcept.
*/
template <class ValueType>
class ReadViewFwdRange
{
@@ -53,6 +109,18 @@ public:
"ReadViewFwdRange move and move assign constructors should be "
"noexcept");
/** STL forward iterator over a `ReadViewFwdRange`.
*
* Value-type wrapper around a heap-allocated `iter_base`. Copy uses
* `iter_base::copy()` for a polymorphic deep clone; move transfers
* ownership of the `unique_ptr` without allocation and is `noexcept`.
* Dereference results are cached in `cache_` and cleared on advance,
* amortizing the cost of repeated `*it` or `it->` calls in tight loops.
*
* @note Comparing iterators from different views triggers an
* `XRPL_ASSERT` in debug builds. The `view_` pointer is carried
* solely for this cross-view sanity check.
*/
class Iterator
{
public:
@@ -66,43 +134,127 @@ public:
using iterator_category = std::forward_iterator_tag;
/** Constructs a singular (default) iterator.
*
* A default-constructed iterator is not dereferenceable and must
* not be incremented. It compares equal only to other
* default-constructed iterators.
*/
Iterator() = default;
/** Copy-constructs an independent iterator at the same position.
*
* Calls `iter_base::copy()` to deep-clone the polymorphic
* implementation, producing a new iterator that advances
* independently of @p other.
*
* @param other The iterator to clone.
*/
Iterator(Iterator const& other);
/** Move-constructs an iterator, transferring ownership of the impl.
*
* @param other The iterator to move from; left in a valid but
* singular state.
*/
Iterator(Iterator&& other) noexcept;
// Used by the implementation
/** Constructs an iterator from a raw view pointer and a polymorphic impl.
*
* Used exclusively by `ReadView`'s factory methods (`slesBegin()`,
* `slesEnd()`, etc.). Not intended for direct use by callers.
*
* @param view The owning view; stored only for cross-view assertion.
* @param impl The heap-allocated concrete iterator; ownership is
* transferred to this object.
*/
explicit Iterator(ReadView const* view, std::unique_ptr<iter_base> impl);
/** Copy-assigns from another iterator at the same position.
*
* Deep-clones via `iter_base::copy()`.
*
* @param other The iterator to copy.
* @return `*this`.
*/
Iterator&
operator=(Iterator const& other);
/** Move-assigns from another iterator.
*
* @param other The iterator to move from; left in a valid but
* singular state.
* @return `*this`.
*/
Iterator&
operator=(Iterator&& other) noexcept;
/** Returns `true` if both iterators denote the same position.
*
* Delegates to `iter_base::equal()`. Two null `impl_` pointers also
* compare equal (both are end sentinels / default-constructed).
*
* @param other The iterator to compare against.
* @return `true` when both iterators are at the same element.
* @note Asserts in debug builds that both iterators belong to the
* same view. Comparing iterators from different views is
* undefined behaviour.
*/
bool
operator==(Iterator const& other) const;
/** Returns `true` if the iterators denote different positions.
*
* @param other The iterator to compare against.
* @return `true` when the iterators are not at the same element.
*/
bool
operator!=(Iterator const& other) const;
/** Returns a reference to the current element.
*
* The result is cached after the first call; subsequent calls before
* the next `operator++` return the cached value at no extra cost.
*
* @return A `const` reference to the current `ValueType`.
* @throw May throw if the underlying `iter_base::dereference()` call fails.
*/
// Can throw
reference
operator*() const;
/** Returns a pointer to the current element.
*
* Delegates to `operator*()` so caching and exception behaviour are
* identical to that of the dereference operator.
*
* @return A `const` pointer to the current `ValueType`.
* @throw May throw if the underlying `iter_base::dereference()` call fails.
*/
// Can throw
pointer
operator->() const;
/** Advances the iterator and clears the dereference cache.
*
* @return `*this` after advancing to the next element.
*/
Iterator&
operator++();
/** Returns a copy of the current iterator, then advances.
*
* @return An iterator to the element before the advance.
*/
Iterator
operator++(int);
private:
/** Owning view; compared in `operator==` to catch cross-view misuse. */
ReadView const* view_ = nullptr;
/** Heap-allocated polymorphic iterator; null for the end sentinel. */
std::unique_ptr<iter_base> impl_{};
/** One-slot dereference cache; cleared on each advance. */
std::optional<value_type> mutable cache_;
};
@@ -118,11 +270,19 @@ public:
ReadViewFwdRange&
operator=(ReadViewFwdRange const&) = default;
/** Constructs a range bound to @p view.
*
* The range stores a raw pointer to the view. The view must outlive
* the range and any iterators derived from it.
*
* @param view The `ReadView` whose factory methods supply iterators.
*/
explicit ReadViewFwdRange(ReadView const& view) : view_(&view)
{
}
protected:
/** The view whose factory methods supply concrete `iter_base` instances. */
ReadView const* view_;
};

View File

@@ -1,3 +1,22 @@
/** @file
* Mathematical and operational backbone of the XRPL Automated Market Maker.
*
* Provides every computation needed to run a constant-product AMM pool:
* LP token minting and burning (XLS-30d Equations 3, 4, 7, 8), spot-price
* quality alignment against the central limit order book, swap execution with
* rigorous directional rounding, and ledger-state helpers for pool balance
* queries and AMM account lifecycle management.
*
* All arithmetic observes the pool invariant:
* @code
* sqrt(poolAsset1 × poolAsset2) >= LPTokenBalance
* @endcode
* Rounding is always directed to keep the pool at least as large as required.
* The `fixAMMv1_1` amendment introduced per-step directional rounding for
* swaps; `fixAMMv1_3` extended this discipline to LP token and
* deposit/withdrawal formulas. Pre-amendment paths are preserved for
* historic ledger replay.
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -22,6 +41,17 @@ namespace xrpl {
namespace detail {
/** Scale @p amount down by 99.99% as a last-resort quality rescue.
*
* When the rounded offer from `getAMMOfferStartWithTakerGets` or
* `getAMMOfferStartWithTakerPays` still falls below the target quality due
* to XRP integer-drop discretization, this function shrinks it by 0.01%
* (rounding toward zero) so the resulting offer quality meets or exceeds
* the target without generating an implausibly small trade.
*
* @param amount The offer side (takerGets or takerPays) to reduce.
* @return The reduced amount, or zero if already at zero.
*/
Number
reduceOffer(auto const& amount)
{
@@ -34,22 +64,41 @@ reduceOffer(auto const& amount)
} // namespace detail
/** Direction tag used throughout deposit/withdrawal and rounding helpers.
*
* Passed to functions that behave asymmetrically between deposit (LP tokens
* rounded down, assets rounded up) and withdrawal (LP tokens rounded up,
* assets rounded down) to preserve the pool invariant.
*/
enum class IsDeposit : bool { No = false, Yes = true };
/** Calculate LP Tokens given AMM pool reserves.
* @param asset1 AMM one side of the pool reserve
* @param asset2 AMM another side of the pool reserve
* @return LP Tokens as IOU
/** Compute the initial LP token supply for a newly seeded AMM pool.
*
* Uses the geometric mean `sqrt(asset1 × asset2)`, which sets the
* pool invariant to equality at creation: `sqrt(asset1 × asset2) == LPTokens`.
* Under `fixAMMv1_3` the result is rounded downward so the pool starts
* with a slight surplus, preserving the invariant.
*
* @param asset1 Balance of the first pool asset.
* @param asset2 Balance of the second pool asset.
* @param lptIssue Asset descriptor identifying the LP token currency/issuer.
* @return Initial LP token amount as an IOU `STAmount`.
*/
STAmount
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue);
/** Calculate LP Tokens given asset's deposit amount.
* @param asset1Balance current AMM asset1 balance
* @param asset1Deposit requested asset1 deposit amount
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return tokens
/** LP tokens minted for a single-asset deposit (XLS-30d Equation 3).
*
* A single-sided deposit is economically equivalent to a proportional
* deposit plus a fee-bearing swap; the fee is embedded via `feeMult` and
* `feeMultHalf`. Under `fixAMMv1_3` the final multiplication is rounded
* downward so fewer tokens are issued, preserving the pool invariant.
*
* @param asset1Balance Current pool balance of the asset being deposited.
* @param asset1Deposit Amount being deposited.
* @param lptAMMBalance Current total LP token supply.
* @param tfee Trading fee in basis points (e.g. 1000 = 1%).
* @return LP tokens to mint for the depositor.
*/
STAmount
lpTokensOut(
@@ -58,12 +107,19 @@ lpTokensOut(
STAmount const& lptAMMBalance,
std::uint16_t tfee);
/** Calculate asset deposit given LP Tokens.
* @param asset1Balance current AMM asset1 balance
* @param lpTokens LP Tokens
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return
/** Asset deposit required to receive a given number of LP tokens (XLS-30d Equation 4).
*
* Inverse of `lpTokensOut`: solves Equation 3 for the deposit amount given a
* desired token output. The solution is a quadratic whose positive root is
* found via `solveQuadraticEq`. Under `fixAMMv1_3` the result is rounded
* upward so the depositor contributes slightly more, preserving the pool
* invariant.
*
* @param asset1Balance Current pool balance of the asset to deposit.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens Desired LP token amount.
* @param tfee Trading fee in basis points.
* @return Asset amount the depositor must contribute.
*/
STAmount
ammAssetIn(
@@ -72,13 +128,18 @@ ammAssetIn(
STAmount const& lpTokens,
std::uint16_t tfee);
/** Calculate LP Tokens given asset's withdraw amount. Return 0
* if can't calculate.
* @param asset1Balance current AMM asset1 balance
* @param asset1Withdraw requested asset1 withdraw amount
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return tokens out amount
/** LP tokens to burn for a single-asset withdrawal (XLS-30d Equation 7).
*
* Computes how many LP tokens must be redeemed to withdraw a specified asset
* amount. Returns zero if the inputs make calculation impossible. Under
* `fixAMMv1_3` the final multiplication is rounded upward so more tokens must
* be burned, preserving the pool invariant.
*
* @param asset1Balance Current pool balance of the asset being withdrawn.
* @param asset1Withdraw Requested withdrawal amount.
* @param lptAMMBalance Current total LP token supply.
* @param tfee Trading fee in basis points.
* @return LP tokens the withdrawer must burn, or zero if the calculation fails.
*/
STAmount
lpTokensIn(
@@ -87,12 +148,18 @@ lpTokensIn(
STAmount const& lptAMMBalance,
std::uint16_t tfee);
/** Calculate asset withdrawal by tokens
* @param assetBalance balance of the asset being withdrawn
* @param lptAMMBalance total AMM Tokens balance
* @param lpTokens LP Tokens balance
* @param tfee trading fee in basis points
* @return calculated asset amount
/** Asset returned when burning a given number of LP tokens (XLS-30d Equation 8).
*
* Inverse of `lpTokensIn`: solves Equation 7 for the withdrawal amount given
* the token burn. Under `fixAMMv1_3` the final multiplication is rounded
* downward so the withdrawer receives slightly less, preserving the pool
* invariant.
*
* @param assetBalance Current pool balance of the asset to withdraw.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens LP tokens being burned.
* @param tfee Trading fee in basis points.
* @return Asset amount returned to the withdrawer.
*/
STAmount
ammAssetOut(
@@ -101,12 +168,19 @@ ammAssetOut(
STAmount const& lpTokens,
std::uint16_t tfee);
/** Check if the relative distance between the qualities
* is within the requested distance.
* @param calcQuality calculated quality
* @param reqQuality requested quality
* @param dist requested relative distance
* @return true if within dist, false otherwise
/** Check whether two `Quality` values are within a relative tolerance.
*
* `Quality` has no subtraction operator, so the comparison is performed via
* `Quality::rate()`, which returns the *inverse* of quality (output/input).
* The formula `(min.rate - max.rate) / min.rate < dist` is equivalent to
* the standard `(max - min) / max < dist` after accounting for the inversion.
* Used in `changeSpotPriceQuality` to suppress trace-level errors when the
* quality mismatch is within one part in ten million (1e-7).
*
* @param calcQuality Computed quality.
* @param reqQuality Target quality.
* @param dist Maximum acceptable relative distance (e.g. `Number(1, -7)`).
* @return `true` if the two qualities are within @p dist of each other.
*/
inline bool
withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Number const& dist)
@@ -120,12 +194,18 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu
return ((min.rate() - max.rate()) / min.rate()) < dist;
}
/** Check if the relative distance between the amounts
* is within the requested distance.
* @param calc calculated amount
* @param req requested amount
* @param dist requested relative distance
* @return true if within dist, false otherwise
/** Check whether two numeric amounts are within a relative tolerance.
*
* Computes `(max - min) / max` and tests that it is less than @p dist.
* Accepted for `STAmount`, `IOUAmount`, `XRPAmount`, `MPTAmount`, and
* `Number`. Used alongside the `Quality` overload to emit quality-mismatch
* errors only when the discrepancy is truly significant.
*
* @tparam Amt Amount type; constrained to the five types listed above.
* @param calc Computed amount.
* @param req Target amount.
* @param dist Maximum acceptable relative distance.
* @return `true` if the two amounts are within @p dist of each other.
*/
template <typename Amt>
requires(
@@ -141,34 +221,49 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist)
return ((max - min) / max) < dist;
}
/** Solve quadratic equation to find takerGets or takerPays. Round
* to minimize the amount in order to maximize the quality.
/** Smallest positive root of `a·x² + b·x + c = 0`, used to minimize offer size.
*
* Uses the numerically stable "citardauq" formula (Blinn 2006): when `b > 0`
* it computes `2c / (-b - sqrt(d))` instead of the standard
* `(-b + sqrt(d)) / 2a`, avoiding catastrophic cancellation when the two
* terms in the numerator are nearly equal. Minimizing the root maximizes
* offer quality in `getAMMOfferStartWithTakerGets` / `getAMMOfferStartWithTakerPays`.
*
* @param a Quadratic coefficient.
* @param b Linear coefficient.
* @param c Constant term.
* @return The smallest positive root, or `std::nullopt` if the discriminant
* is negative (no real solution) or the root is non-positive.
*/
std::optional<Number>
solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c);
/** Generate AMM offer starting with takerGets when AMM pool
* from the payment perspective is IOU(in)/XRP(out)
* Equations:
* Spot Price Quality after the offer is consumed:
* Qsp = (O - o) / (I + i) -- equation (1)
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
* Swap out:
* i = (I * o) / (O - o) * f -- equation (2)
* where f is (1 - tfee/100000), tfee is in basis points
* Effective price targetQuality:
* Qep = o / i -- equation (3)
* There are two scenarios to consider
* A) Qsp = Qep. Substitute i in (1) with (2) and solve for o
* and Qsp = targetQuality(Qt):
* o**2 + o * (I * Qt * (1 - 1 / f) - 2 * O) + O**2 - Qt * I * O = 0
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for o
* and Qep = targetQuality(Qt):
* o = O - I * Qt / f
* Since the scenario is not known a priori, both A and B are solved and
* the lowest value of o is takerGets. takerPays is calculated with
* swap out eq (2). If o is less or equal to 0 then the offer can't
* be generated.
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
* starting from takerGets (XRP out, IOU in).
*
* Used when the pool pays XRP (IOU-in / XRP-out). Starting from the XRP
* side ensures that rounding XRP down to integer drops improves rather than
* degrades offer quality (post-`fixAMMv1_1` behavior).
*
* Two binding constraints are solved and the smaller takerGets is chosen:
* - Scenario A — post-swap spot price equals @p targetQuality:
* `o² + o·(I·Qt·(1 - 1/f) - 2·O) + O² - Qt·I·O = 0`
* - Scenario B — effective offer price equals @p targetQuality:
* `o = O - I·Qt / f`
*
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
* takerPays is then derived from the swap-out equation. If the resulting
* offer quality is still below @p targetQuality after rounding, a 99.99%
* rescale via `detail::reduceOffer` is attempted.
*
* @tparam TIn Asset type flowing into the pool (IOU side).
* @tparam TOut Asset type flowing out of the pool (XRP side).
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
* @param targetQuality Desired offer quality (CLOB best quality).
* @param tfee Trading fee in basis points.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
* valid offer cannot be generated (e.g. target quality unreachable at
* current fee).
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -214,28 +309,30 @@ getAMMOfferStartWithTakerGets(
return amounts;
}
/** Generate AMM offer starting with takerPays when AMM pool
* from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out).
* Equations:
* Spot Price Quality after the offer is consumed:
* Qsp = (O - o) / (I + i) -- equation (1)
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
* Swap in:
* o = (O * i * f) / (I + i * f) -- equation (2)
* where f is (1 - tfee/100000), tfee is in basis points
* Effective price quality:
* Qep = o / i -- equation (3)
* There are two scenarios to consider
* A) Qsp = Qep. Substitute o in (1) with (2) and solve for i
* and Qsp = targetQuality(Qt):
* i**2 * f + i * I * (1 + f) + I**2 - I * O / Qt = 0
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for i
* and Qep = targetQuality(Qt):
* i = O / Qt - I / f
* Since the scenario is not known a priori, both A and B are solved and
* the lowest value of i is takerPays. takerGets is calculated with
* swap in eq (2). If i is less or equal to 0 then the offer can't
* be generated.
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
* starting from takerPays (XRP in, or IOU/IOU).
*
* Used for XRP-in/IOU-out and IOU/IOU pools. Starting from the XRP
* side (takerPays) under `fixAMMv1_1` keeps rounding effects favorable.
*
* Two binding constraints are solved and the smaller takerPays is chosen:
* - Scenario A — post-swap spot price equals @p targetQuality:
* `i²·f + i·I·(1+f) + I² - I·O/Qt = 0`
* - Scenario B — effective offer price equals @p targetQuality:
* `i = O/Qt - I/f`
*
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
* takerGets is then derived from the swap-in equation. If the resulting
* offer quality is still below @p targetQuality after rounding, a 99.99%
* rescale via `detail::reduceOffer` is attempted.
*
* @tparam TIn Asset type flowing into the pool.
* @tparam TOut Asset type flowing out of the pool.
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
* @param targetQuality Desired offer quality (CLOB best quality).
* @param tfee Trading fee in basis points.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
* valid offer cannot be generated.
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -281,21 +378,34 @@ getAMMOfferStartWithTakerPays(
return amounts;
}
/** Generate AMM offer so that either updated Spot Price Quality (SPQ)
* is equal to LOB quality (in this case AMM offer quality is
* better than LOB quality) or AMM offer is equal to LOB quality
* (in this case SPQ is better than LOB quality).
* Pre-amendment code calculates takerPays first. If takerGets is XRP,
* it is rounded down, which results in worse offer quality than
* LOB quality, and the offer might fail to generate.
* Post-amendment code calculates the XRP offer side first. The result
* is rounded down, which makes the offer quality better.
* It might not be possible to match either SPQ or AMM offer to LOB
* quality. This generally happens at higher fees.
* @param pool AMM pool balances
* @param quality requested quality
* @param tfee trading fee in basis points
* @return seated in/out amounts if the quality can be changed
/** Generate a synthetic AMM offer that aligns the pool's spot price with a CLOB quality.
*
* The payment engine calls this when it encounters both AMM pools and order
* book offers for the same currency pair. The resulting offer has a quality
* such that either the post-swap spot price equals @p quality (AMM offer
* quality is better) or the offer's effective price equals @p quality (the
* post-swap spot price is better) — whichever produces the smaller offer.
*
* Amendment behavior:
* - Pre-`fixAMMv1_1`: always solves for takerPays first; rounding down XRP
* takerGets can push quality below target, causing the offer to be rejected.
* - Post-`fixAMMv1_1`: solves for the XRP side first (takerGets when pool pays
* XRP, takerPays otherwise) so XRP rounding improves rather than degrades
* quality. Falls back to `detail::reduceOffer` if quality is still below
* target after rounding.
*
* A quality mismatch larger than 1e-7 is logged at `j.error()` level; smaller
* mismatches are trace-only.
*
* @tparam TIn Asset type flowing into the pool.
* @tparam TOut Asset type flowing out of the pool.
* @param pool Current AMM pool balances.
* @param quality Target quality (best CLOB offer quality for this pair).
* @param tfee Trading fee in basis points.
* @param rules Current ledger rules (for amendment checks).
* @param j Journal for diagnostic logging.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if the
* quality cannot be achieved (generally at high fees).
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -398,26 +508,26 @@ changeSpotPriceQuality(
return amounts;
}
/** AMM pool invariant - the product (A * B) after swap in/out has to remain
* at least the same: (A + in) * (B - out) >= A * B
* XRP round-off may result in a smaller product after swap in/out.
* To address this:
* - if on swapIn the out is XRP then the amount is round-off
* downward, making the product slightly larger since out
* value is reduced.
* - if on swapOut the in is XRP then the amount is round-off
* upward, making the product slightly larger since in
* value is increased.
*/
// --- Swap-in / Swap-out ---
/** Swap assetIn into the pool and swap out a proportional amount
* of the other asset. Implements AMM Swap in.
* @see [XLS30d:AMM
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
* @param pool current AMM pool balances
* @param assetIn amount to swap in
* @param tfee trading fee in basis points
* @return
/** Deposit @p assetIn into the pool and receive a proportional amount of the
* other asset (AMM Swap in, XLS-30d).
*
* Formula: `out = pool.out - (pool.in × pool.out) / (pool.in + assetIn × feeMult(tfee))`
*
* Pool invariant: `(pool.in + assetIn) × (pool.out - out) >= pool.in × pool.out`.
* XRP integer rounding can violate this; post-`fixAMMv1_1` each sub-expression
* has an explicitly directed rounding mode so the pool retains a tiny surplus.
* The output is always rounded downward so the trader receives less, not more.
*
* @tparam TIn Asset type deposited (poolGets side).
* @tparam TOut Asset type received (poolPays side).
* @param pool Current AMM pool balances.
* @param assetIn Amount being deposited into the pool.
* @param tfee Trading fee in basis points.
* @return Amount of the output asset the trader receives; zero if the pool
* denominator is non-positive.
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
template <typename TIn, typename TOut>
TOut
@@ -476,14 +586,23 @@ swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t t
Number::RoundingMode::Downward);
}
/** Swap assetOut out of the pool and swap in a proportional amount
* of the other asset. Implements AMM Swap out.
* @see [XLS30d:AMM
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
* @param pool current AMM pool balances
* @param assetOut amount to swap out
* @param tfee trading fee in basis points
* @return
/** Withdraw @p assetOut from the pool and compute the required input asset (AMM Swap out, XLS-30d).
*
* Formula: `in = ((pool.in × pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee)`
*
* The input is always rounded upward so the trader pays at least what the
* pool needs to maintain its invariant. Post-`fixAMMv1_1` each intermediate
* step is individually directed; if the pool denominator is non-positive (i.e.
* @p assetOut >= the entire pool), the maximum representable `TIn` is returned.
*
* @tparam TIn Asset type deposited (poolGets side).
* @tparam TOut Asset type withdrawn (poolPays side).
* @param pool Current AMM pool balances.
* @param assetOut Amount being withdrawn from the pool.
* @param tfee Trading fee in basis points.
* @return Amount of the input asset the trader must pay; `toMaxAmount<TIn>`
* if the requested output would exhaust the pool.
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
template <typename TIn, typename TOut>
TIn
@@ -542,35 +661,46 @@ swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_
Number::RoundingMode::Upward);
}
/** Return square of n.
*/
/** Return `n²`. */
Number
square(Number const& n);
/** Adjust LP tokens to deposit/withdraw.
* Amount type keeps 16 digits. Maintaining the LP balance by adding
* deposited tokens or subtracting withdrawn LP tokens from LP balance
* results in losing precision in LP balance. I.e. the resulting LP balance
* is less than the actual sum of LP tokens. To adjust for this, subtract
* old tokens balance from the new one for deposit or vice versa for
* withdraw to cancel out the precision loss.
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param isDeposit Yes if deposit, No if withdraw
/** Adjust LP tokens to account for 16-digit precision loss in the running balance.
*
* Adding newly-minted tokens to an already-large `lptAMMBalance` can lose
* significance in the least-significant digit: the stored balance advances
* by less than `lpTokens`. This function round-trips through the 16-digit
* representation by computing `(balance + tokens) - balance` (deposit) or
* `(tokens - balance) + balance` (withdraw), returning the value that will
* actually be committed to the ledger. Result is forced downward to ensure
* the adjusted tokens do not exceed the requested tokens.
*
* @param lptAMMBalance Current total LP token supply stored on the AMM SLE.
* @param lpTokens Tokens being minted or burned.
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
* @return Adjusted token amount that exactly matches the representable delta
* in the 16-digit balance.
*/
STAmount
adjustLPTokens(STAmount const& lptAMMBalance, STAmount const& lpTokens, IsDeposit isDeposit);
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
* the adjusted LP tokens are less than the provided LP tokens.
* @param amountBalance asset1 pool balance
* @param amount asset1 to deposit or withdraw
* @param amount2 asset2 to deposit or withdraw
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param tfee trading fee in basis points
* @param isDeposit Yes if deposit, No if withdraw
* @return
/** Adjust deposit/withdrawal asset amounts to match the precision-corrected LP token count.
*
* Calls `adjustLPTokens()` to compute the representable token delta. If the
* adjusted count is less than @p lpTokens, the corresponding asset amounts are
* scaled down so the ledger does not grant assets that exceed what the LP token
* math supports. A no-op when `fixAMMv1_3` is active because `getRoundedLPTokens`
* already incorporates the precision adjustment.
*
* @param amountBalance Current pool balance of the primary asset.
* @param amount Primary asset amount to deposit or withdraw.
* @param amount2 Secondary asset amount for two-sided operations; `std::nullopt`
* for single-asset operations.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens Calculated LP tokens before precision adjustment.
* @param tfee Trading fee in basis points.
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
* @return Tuple of `(adjustedAmount, adjustedAmount2, adjustedLPTokens)`.
*/
std::tuple<STAmount, std::optional<STAmount>, STAmount>
adjustAmountsByLPTokens(
@@ -582,17 +712,46 @@ adjustAmountsByLPTokens(
std::uint16_t tfee,
IsDeposit isDeposit);
/** Positive solution for quadratic equation:
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
/** Positive root of `a·x² + b·x + c = 0` using the standard formula.
*
* Computes `x = (-b + sqrt(b² - 4·a·c)) / (2·a)`. Used by `ammAssetIn`
* to invert Equation 4; the discriminant is guaranteed non-negative by the
* deposit formula's domain.
*
* @param a Quadratic coefficient.
* @param b Linear coefficient.
* @param c Constant term.
* @return The positive root.
*/
Number
solveQuadraticEq(Number const& a, Number const& b, Number const& c);
/** Multiply @p amount by @p frac with an explicitly directed rounding mode.
*
* Installs @p rm for both the `Number` multiplication and the subsequent
* `toSTAmount` conversion so that rounding is applied once at the final step,
* not accumulated through intermediates. This is the building block for all
* `fixAMMv1_3` directional-rounding paths.
*
* @param amount Base `STAmount` to scale.
* @param frac Scaling factor.
* @param rm Rounding mode to apply at the final conversion step.
* @return `amount × frac` rounded according to @p rm, expressed in the same
* asset as @p amount.
*/
STAmount
multiply(STAmount const& amount, Number const& frac, Number::RoundingMode rm);
namespace detail {
/** Select the LP token rounding direction that preserves the pool invariant.
*
* Deposit: round downward (fewer tokens minted → pool worth more per token).
* Withdraw: round upward (more tokens burned → pool retains slightly more).
*
* @param isDeposit Direction of the operation.
* @return `Downward` for deposit, `Upward` for withdrawal.
*/
inline Number::RoundingMode
getLPTokenRounding(IsDeposit isDeposit)
{
@@ -602,6 +761,14 @@ getLPTokenRounding(IsDeposit isDeposit)
: Number::RoundingMode::Upward;
}
/** Select the asset rounding direction that preserves the pool invariant.
*
* Deposit: round upward (depositor pays slightly more → pool is larger).
* Withdraw: round downward (withdrawer receives slightly less → pool retains).
*
* @param isDeposit Direction of the operation.
* @return `Upward` for deposit, `Downward` for withdrawal.
*/
inline Number::RoundingMode
getAssetRounding(IsDeposit isDeposit)
{
@@ -613,10 +780,19 @@ getAssetRounding(IsDeposit isDeposit)
} // namespace detail
/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas
* calculate the amount as a fractional value of the pool balance. The rounding
* takes place on the last step of multiplying the balance by the fraction if
* AMMv1_3 is enabled.
/** Compute a proportional asset amount with amendment-gated directional rounding.
*
* Used for two-sided (equal) deposit/withdrawal where the asset amount is
* `balance × frac`. Under `fixAMMv1_3` the final multiplication is rounded
* via `detail::getAssetRounding` (upward on deposit, downward on withdraw).
* Without the amendment the result uses the current ambient rounding mode.
*
* @tparam A Type of @p frac; either `STAmount` or `Number`.
* @param rules Current ledger rules.
* @param balance Pool balance of the asset.
* @param frac Fraction of the pool balance to apply.
* @param isDeposit Direction; controls rounding when `fixAMMv1_3` is active.
* @return `balance × frac` rounded to preserve the pool invariant.
*/
template <typename A>
STAmount
@@ -637,14 +813,20 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe
return multiply(balance, frac, rm);
}
/** Round AMM single deposit/withdrawal amount.
* The lambda's are used to delay evaluation until the function
* is executed so that the calculation is not done twice. noRoundCb() is
* called if AMMv1_3 is disabled. Otherwise, the rounding is set and
* the amount is:
* isDeposit is Yes - the balance multiplied by productCb()
* isDeposit is No - the result of productCb(). The rounding is
* the same for all calculations in productCb()
/** Compute a single-asset deposit/withdrawal amount with amendment-gated rounding.
*
* The callback form defers evaluation to avoid computing the formula twice:
* - Without `fixAMMv1_3`: calls `noRoundCb()` and converts without directed rounding.
* - With `fixAMMv1_3`, deposit: calls `multiply(balance, productCb(), rm)`.
* - With `fixAMMv1_3`, withdrawal: installs @p rm globally and calls `productCb()`
* so every arithmetic step inside the callback shares the same rounding direction.
*
* @param rules Current ledger rules.
* @param noRoundCb Produces the unrounded result (pre-amendment path).
* @param balance Pool balance of the asset.
* @param productCb Produces the rounding fraction (post-amendment path).
* @param isDeposit Direction; controls which rounding mode is selected.
* @return Rounded asset amount preserving the pool invariant.
*/
STAmount
getRoundedAsset(
@@ -654,12 +836,18 @@ getRoundedAsset(
std::function<Number()> const& productCb,
IsDeposit isDeposit);
/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas
* calculate the lptokens as a fractional value of the AMM total lptokens.
* The rounding takes place on the last step of multiplying the balance by
* the fraction if AMMv1_3 is enabled. The tokens are then
* adjusted to factor in the loss in precision (we only keep 16 significant
* digits) when adding the lptokens to the balance.
/** Compute a proportional LP token amount with amendment-gated rounding and precision adjustment.
*
* Used for two-sided (equal) deposit/withdrawal. Under `fixAMMv1_3` the
* multiplication `balance × frac` is rounded via `detail::getLPTokenRounding`,
* then `adjustLPTokens` corrects for the 16-digit precision loss introduced
* when adding the result to the running LP token balance.
*
* @param rules Current ledger rules.
* @param balance Current total LP token supply.
* @param frac Fraction of the pool's LP supply to mint or burn.
* @param isDeposit Direction; controls rounding and sign of the adjustment.
* @return LP token amount after rounding and precision correction.
*/
STAmount
getRoundedLPTokens(
@@ -668,16 +856,22 @@ getRoundedLPTokens(
Number const& frac,
IsDeposit isDeposit);
/** Round AMM single deposit/withdrawal LPToken amount.
* The lambda's are used to delay evaluation until the function is executed
* so that the calculations are not done twice.
* noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set
* and the lptokens are:
* if isDeposit is Yes - the result of productCb(). The rounding is
* the same for all calculations in productCb()
* if isDeposit is No - the balance multiplied by productCb()
* The lptokens are then adjusted to factor in the loss in precision
* (we only keep 16 significant digits) when adding the lptokens to the balance.
/** Compute a single-asset LP token amount with amendment-gated rounding and precision adjustment.
*
* The callback form avoids evaluating the formula twice:
* - Without `fixAMMv1_3`: calls `noRoundCb()` with no directed rounding.
* - With `fixAMMv1_3`, deposit: installs the LP rounding mode globally and
* calls `productCb()` (all arithmetic inside shares the direction).
* - With `fixAMMv1_3`, withdrawal: calls `multiply(lptAMMBalance, productCb(), rm)`.
* In all post-amendment cases, `adjustLPTokens` then corrects for 16-digit
* precision loss in the running LP balance.
*
* @param rules Current ledger rules.
* @param noRoundCb Produces the unrounded result (pre-amendment path).
* @param lptAMMBalance Current total LP token supply.
* @param productCb Produces the rounding fraction (post-amendment path).
* @param isDeposit Direction; controls rounding mode selection.
* @return LP token amount after rounding and precision correction.
*/
STAmount
getRoundedLPTokens(
@@ -687,16 +881,21 @@ getRoundedLPTokens(
std::function<Number()> const& productCb,
IsDeposit isDeposit);
/* Next two functions adjust asset in/out amount to factor in the adjusted
* lptokens. The lptokens are calculated from the asset in/out. The lptokens are
* then adjusted to factor in the loss in precision. The adjusted lptokens might
* be less than the initially calculated tokens. Therefore, the asset in/out
* must be adjusted. The rounding might result in the adjusted amount being
* greater than the original asset in/out amount. If this happens,
* then the original amount is reduced by the difference in the adjusted amount
* and the original amount. The actual tokens and the actual adjusted amount
* are then recalculated. The minimum of the original and the actual
* adjusted amount is returned.
/** Adjust a single-asset deposit amount to match the precision-corrected LP token count.
*
* Under `fixAMMv1_3`: computes `ammAssetIn(balance, lptAMMBalance, tokens, tfee)`.
* If rounding causes the derived asset amount to exceed @p amount, the deposit is
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
* of original and adjusted amounts is returned. Before the amendment, returns the
* inputs unchanged.
*
* @param rules Current ledger rules.
* @param balance Pool balance of the asset being deposited.
* @param amount Requested deposit amount.
* @param lptAMMBalance Current total LP token supply.
* @param tokens LP token count before precision adjustment.
* @param tfee Trading fee in basis points.
* @return `{adjustedTokens, adjustedAmount}` pair.
*/
std::pair<STAmount, STAmount>
adjustAssetInByTokens(
@@ -706,6 +905,23 @@ adjustAssetInByTokens(
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee);
/** Adjust a single-asset withdrawal amount to match the precision-corrected LP token count.
*
* Under `fixAMMv1_3`: computes `ammAssetOut(balance, lptAMMBalance, tokens, tfee)`.
* If rounding causes the derived asset amount to exceed @p amount, the withdrawal is
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
* of original and adjusted amounts is returned. Before the amendment, returns the
* inputs unchanged.
*
* @param rules Current ledger rules.
* @param balance Pool balance of the asset being withdrawn.
* @param amount Requested withdrawal amount.
* @param lptAMMBalance Current total LP token supply.
* @param tokens LP token count before precision adjustment.
* @param tfee Trading fee in basis points.
* @return `{adjustedTokens, adjustedAmount}` pair.
*/
std::pair<STAmount, STAmount>
adjustAssetOutByTokens(
Rules const& rules,
@@ -715,8 +931,20 @@ adjustAssetOutByTokens(
STAmount const& tokens,
std::uint16_t tfee);
/** Find a fraction of tokens after the tokens are adjusted. The fraction
* is used to adjust equal deposit/withdraw amount.
/** Recompute the LP token fraction after precision adjustment.
*
* Under `fixAMMv1_3` the precision-adjusted token count may differ from the
* originally requested count, so the fraction `tokens / lptAMMBalance` must
* be recomputed from the adjusted value before it is used to scale equal
* deposit/withdrawal amounts. Returns @p frac unchanged when `fixAMMv1_3`
* is inactive (the precision adjustment has not yet been applied).
*
* @param rules Current ledger rules.
* @param lptAMMBalance Current total LP token supply.
* @param tokens Precision-adjusted LP token count.
* @param frac Original fraction before adjustment.
* @return Adjusted fraction `tokens / lptAMMBalance`, or @p frac if
* `fixAMMv1_3` is not active.
*/
Number
adjustFracByTokens(
@@ -725,7 +953,19 @@ adjustFracByTokens(
STAmount const& tokens,
Number const& frac);
/** Get AMM pool balances.
/** Read the AMM's current pool asset balances from the ledger.
*
* Delegates to `accountHolds` for each asset, respecting freeze and
* authorization policy. Does not read the LP token balance.
*
* @param view Ledger state to query.
* @param ammAccountID AccountID of the AMM's pseudo-account.
* @param asset1 First pool asset.
* @param asset2 Second pool asset.
* @param freezeHandling Whether to enforce freeze restrictions.
* @param authHandling Whether to enforce authorization restrictions.
* @param j Journal for diagnostic logging.
* @return `{balance1, balance2}` pair in the same asset order as the inputs.
*/
std::pair<STAmount, STAmount>
ammPoolHolds(
@@ -737,9 +977,23 @@ ammPoolHolds(
AuthHandling authHandling,
beast::Journal const j);
/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.
/** Read the AMM's pool balances and total LP token supply from the ledger.
*
* When both optional assets are provided they are validated against the AMM
* SLE's stored pair and used as the query order; providing only one resolves
* the counterpart from `ammSle`. If neither is provided, the canonical order
* from `ammSle` is used. An invalid asset pair (mismatched with the AMM SLE)
* indicates a corrupted AMM object and returns `tecAMM_INVALID_TOKENS`.
*
* @param view Ledger state to query.
* @param ammSle The AMM's `ltAMM` SLE.
* @param optAsset1 Optional first asset override.
* @param optAsset2 Optional second asset override.
* @param freezeHandling Whether to enforce freeze restrictions.
* @param authHandling Whether to enforce authorization restrictions.
* @param j Journal for diagnostic logging.
* @return `{balance1, balance2, lpTokenBalance}` on success, or
* `Unexpected(tecAMM_INVALID_TOKENS)` if the asset pair is invalid.
*/
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
@@ -751,7 +1005,21 @@ ammHolds(
AuthHandling authHandling,
beast::Journal const j);
/** Get the balance of LP tokens.
/** Read an LP's token balance from its direct trustline with the AMM account.
*
* Intentionally bypasses `accountHolds` — that function would also check
* whether the AMM's underlying pool assets are frozen (under
* `fixFrozenLPTokenTransfer`), which is incorrect policy for LP token balance
* queries. Only the LP token trustline's own freeze flag is checked.
* Trust-line orientation: raw `sfBalance` is negated when `lpAccount > ammAccount`.
*
* @param view Ledger state to query.
* @param asset1 First pool asset (used to derive the LP token currency).
* @param asset2 Second pool asset.
* @param ammAccount AccountID of the AMM's pseudo-account (LP token issuer).
* @param lpAccount AccountID of the liquidity provider.
* @param j Journal for diagnostic logging.
* @return The LP's token balance, or zero if the trustline is absent or frozen.
*/
STAmount
ammLPHolds(
@@ -762,6 +1030,17 @@ ammLPHolds(
AccountID const& lpAccount,
beast::Journal const j);
/** Read an LP's token balance using the asset pair stored in @p ammSle.
*
* Convenience overload; extracts `sfAsset`, `sfAsset2`, and `sfAccount` from
* @p ammSle and delegates to the five-parameter `ammLPHolds`.
*
* @param view Ledger state to query.
* @param ammSle The AMM's `ltAMM` SLE.
* @param lpAccount AccountID of the liquidity provider.
* @param j Journal for diagnostic logging.
* @return The LP's token balance, or zero if the trustline is absent or frozen.
*/
STAmount
ammLPHolds(
ReadView const& view,
@@ -769,25 +1048,72 @@ ammLPHolds(
AccountID const& lpAccount,
beast::Journal const j);
/** Get AMM trading fee for the given account. The fee is discounted
* if the account is the auction slot owner or one of the slot's authorized
* accounts.
/** Get the effective AMM trading fee for @p account.
*
* Returns the auction slot's `sfDiscountedFee` if the slot is unexpired and
* @p account is either the slot owner or one of up to four authorized accounts;
* otherwise returns the AMM's global `sfTradingFee`. Expiration is compared
* against the ledger's `parentCloseTime` (the slot stores
* `parentCloseTime + TOTAL_TIME_SLOT_SECS` at creation, i.e. 24 hours).
*
* @param view Ledger state providing the current close time.
* @param ammSle The AMM's `ltAMM` SLE.
* @param account The account whose fee rate is needed.
* @return Fee rate in basis points (01000).
*/
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account);
/** Returns total amount held by AMM for the given token.
/** Read the AMM account's raw pool-asset balance, bypassing balance hooks.
*
* Unlike `accountHolds`, this function does not invoke `balanceHookIOU` or
* `balanceHookMPT`, so the result is unaffected by `PaymentSandbox`
* deferred-credit accounting. Used when the AMM needs its own unmodified
* balance for math, not for payment routing. Returns zero if the trustline
* or MPToken object is absent or frozen.
*
* @param view Ledger state to query.
* @param ammAccountID AccountID of the AMM's pseudo-account.
* @param asset The pool asset to query (IOU, XRP, or MPT).
* @return The raw balance, or zero if unavailable.
*/
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset);
/** Delete trustlines to AMM. If all trustlines are deleted then
* AMM object and account are deleted. Otherwise tecINCOMPLETE is returned.
/** Remove all ledger objects owned by the AMM and, if successful, delete the AMM itself.
*
* Deletion is ordered: IOU trustlines first, then MPToken objects, then the
* AMM SLE and its `AccountRoot`. Because each ledger transaction has a bounded
* work budget, not all trustlines may be removable in one call; in that case
* `tecINCOMPLETE` is returned and the caller must submit additional transactions
* to finish. The AMM can be re-deposited while deletion is incomplete.
*
* @param view Sandbox for applying state changes.
* @param asset First pool asset (used to locate the AMM keylet).
* @param asset2 Second pool asset.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on full deletion, `tecINCOMPLETE` if trustlines remain,
* or `tecINTERNAL` for unexpected ledger inconsistencies.
*/
TER
deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j);
/** Initialize Auction and Voting slots and set the trading/discounted fee.
/** Initialize the vote slot and auction slot on a new or re-created AMM.
*
* Called on both `AMMCreate` and on `AMMDeposit` when the pool was previously
* drained to zero. Sets up:
* - One vote entry for @p account with full weight (`kVOTE_WEIGHT_SCALE_FACTOR`).
* - An auction slot owned by @p account, expiring in 24 hours, at zero price.
* - `sfDiscountedFee` = `tfee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`.
* - Absent-field canonicalization: fee fields are removed if their value is zero.
* - Under `fixCleanup3_2_0`, stale `sfAuthAccounts` from any previous slot owner
* are cleared.
*
* @param view Apply-view for the current transaction.
* @param ammSle The AMM's `ltAMM` SLE (modified in place).
* @param account The creator/re-depositor receiving the slot.
* @param lptAsset The LP token asset descriptor (used as the `sfPrice` currency).
* @param tfee Trading fee in basis points to set.
*/
void
initializeFeeAuctionVote(
@@ -797,16 +1123,41 @@ initializeFeeAuctionVote(
Asset const& lptAsset,
std::uint16_t tfee);
/** Return true if the Liquidity Provider is the only AMM provider, false
* otherwise. Return tecINTERNAL if encountered an unexpected condition,
* for instance Liquidity Provider has more than one LPToken trustline.
/** Determine whether @p lpAccount is the sole remaining liquidity provider.
*
* Walks the AMM account's owner directory (up to 10 pages, covering at most
* 4 objects) counting LPToken trustlines, pool-asset trustlines, MPToken
* objects, and the AMM SLE itself. Any second LPToken trustline belonging to
* a different account returns `false` immediately.
*
* @param view Ledger state to query.
* @param ammIssue The LP token issue (currency + AMM account as issuer).
* @param lpAccount AccountID of the candidate sole LP.
* @return `true` if @p lpAccount is the only LP, `false` if other LPs exist,
* or `Unexpected(tecINTERNAL)` for any unexpected directory state
* (e.g. more than one LPToken trustline for @p lpAccount).
*/
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount);
/** Due to rounding, the LPTokenBalance of the last LP might
* not match the LP's trustline balance. If it's within the tolerance,
* update LPTokenBalance to match the LP's trustline balance.
/** Reconcile the AMM's `sfLPTokenBalance` with the last LP's trustline balance.
*
* Accumulated rounding over the life of the pool can cause the AMM's running
* `sfLPTokenBalance` to differ slightly from the sole LP's trustline balance.
* This function:
* 1. Confirms @p account is the only remaining LP via `isOnlyLiquidityProvider`.
* 2. If so, verifies the discrepancy is within 0.1% (tolerance `1e-3`).
* 3. If within tolerance, updates `sfLPTokenBalance` to @p lpTokens so the
* final withdrawal leaves the AMM in a fully consistent state.
*
* @param sb Sandbox for applying the balance correction.
* @param lpTokens The last LP's actual trustline balance.
* @param ammSle The AMM's `ltAMM` SLE (updated in place if correction applied).
* @param account AccountID of the candidate sole LP.
* @return `true` if the balance was reconciled or no adjustment was needed
* (other LPs exist), `Unexpected(tecAMM_INVALID_TOKENS)` if the
* discrepancy exceeds tolerance, or `Unexpected(tecINTERNAL)` on an
* unexpected directory error.
*/
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(

View File

@@ -1,3 +1,12 @@
/** @file
* Free functions for querying and mutating `ltACCOUNT_ROOT` ledger entries.
*
* Provides the canonical helpers for freeze-state queries, spendable XRP
* balance, owner-count bookkeeping, transfer fees, destination-tag
* enforcement, and the creation and detection of pseudo-accounts (AMM,
* Vault, LoanBroker). Almost every transaction processor depends on at
* least one function here.
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -15,26 +24,60 @@
namespace xrpl {
/** Check if the issuer has the global freeze flag set.
@param issuer The account to check
@return true if the account has global freeze set
*/
/** Check whether an IOU issuer has the global freeze flag active.
*
* XRP is never frozen; this function returns `false` immediately for the XRP
* account. For any other issuer it reads `lsfGlobalFreeze` from the
* account root. Missing accounts are treated as non-frozen.
*
* @param view The read-only ledger view to query.
* @param issuer The account whose freeze state is to be checked.
* @return `true` if `issuer` is a non-XRP account with `lsfGlobalFreeze` set;
* `false` otherwise.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, AccountID const& issuer);
// Calculate liquid XRP balance for an account.
// This function may be used to calculate the amount of XRP that
// the holder is able to freely spend. It subtracts reserve requirements.
//
// ownerCountAdj adjusts the owner count in case the caller calculates
// before ledger entries are added or removed. Positive to add, negative
// to subtract.
//
// @param ownerCountAdj positive to add to count, negative to reduce count.
/** Compute the spendable XRP balance for an account after reserve deduction.
*
* Queries the account's current balance and owner count through the view's
* virtual hook methods (`balanceHookIOU`, `ownerCountHook`) so that
* `PaymentSandbox` can overlay uncommitted in-flight changes without any
* branching here. The reserve is then subtracted; if the balance is below
* the reserve, the function returns zero rather than a negative amount.
*
* Pseudo-accounts (AMM, Vault, LoanBroker) bypass the reserve calculation
* entirely and receive the full balance as spendable XRP, because they
* cannot submit transactions and must never be blocked by reserve checks.
*
* @param view The ledger view to query.
* @param id The account whose liquid XRP balance is computed.
* @param ownerCountAdj Signed delta applied to `sfOwnerCount` before the
* reserve is calculated. Pass a positive value when the caller is about
* to add ledger entries; pass a negative value when entries are about to
* be removed. This lets callers reason about post-mutation availability
* before the state is committed to the view.
* @param j Journal for trace-level diagnostics.
* @return The spendable XRP amount, clamped to zero from below.
*/
[[nodiscard]] XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
/** Adjust the owner count up or down. */
/** Increment or decrement `sfOwnerCount` on an account SLE and notify the view.
*
* Delegates to a file-static helper that clamps the result to
* `[0, UINT32_MAX]`, logging at `fatal` severity if either bound would be
* exceeded — silent wrapping of the `uint32_t` field would corrupt ledger
* state. After clamping, `view.adjustOwnerCountHook()` is called before the
* new value is written; `PaymentSandbox` overrides that hook to track the
* high-water-mark count, ensuring subsequent `ownerCountHook` reads use the
* most conservative value seen during the payment.
*
* @param view The mutable view on which the SLE update is recorded.
* @param sle The account SLE to adjust; a null pointer is silently ignored.
* @param amount Signed delta to apply to `sfOwnerCount`; must be non-zero.
* @param j Journal for fatal-level diagnostics on overflow or underflow.
*/
void
adjustOwnerCount(
ApplyView& view,
@@ -42,45 +85,89 @@ adjustOwnerCount(
std::int32_t amount,
beast::Journal j);
/** Returns IOU issuer transfer fee as Rate. Rate specifies
* the fee as fractions of 1 billion. For example, 1% transfer rate
* is represented as 1,010,000,000.
* @param issuer The IOU issuer
/** Return the IOU transfer fee for an issuer as a `Rate` value.
*
* `Rate` expresses the fee as a fraction of one billion, so a 1% fee is
* represented as 1,010,000,000. If the issuer account does not exist or
* has not set `sfTransferRate`, `parityRate` (no fee, i.e., 1,000,000,000)
* is returned — callers never need to handle a null case.
*
* @param view The ledger view to query.
* @param issuer The IOU issuer whose transfer fee is requested.
* @return The issuer's `Rate`, or `parityRate` if none is configured.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, AccountID const& issuer);
/** Generate a pseudo-account address from a pseudo owner key.
@param pseudoOwnerKey The key to generate the address from
@return The generated account ID
*/
/** Derive a collision-free pseudo-account `AccountID` from an owner key.
*
* Iterates up to 256 attempts. Each attempt hashes a counter, the parent
* ledger's hash, and `pseudoOwnerKey` through `sha512Half` then
* `ripesha_hasher` (RIPEMD-160(SHA-256(...))). The parent-hash component
* prevents precomputation of collisions. The first candidate address that
* has no existing `AccountRoot` in `view` is returned.
*
* @param view The ledger view used to check for address collisions.
* @param pseudoOwnerKey The 256-bit key identifying the pseudo-account owner
* (e.g., the AMM or Vault object ID).
* @return A collision-free `AccountID`, or `beast::kZERO` if all 256
* attempts collided. `createPseudoAccount` propagates exhaustion as
* `tecDUPLICATE`.
* @note The 256-attempt cap is consensus-critical and must not be changed
* without an amendment, as it determines the pseudo-account address space.
*/
AccountID
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account
if set.
The list is constructed during initialization and is const after that.
Pseudo-account designator fields MUST be maintained by including the
SField::sMD_PseudoAccount flag in the SField definition.
*/
/** Return the singleton list of `SField`s that designate a pseudo-account.
*
* Built once at first call by scanning the `ltACCOUNT_ROOT` `SOTemplate`
* from `LedgerFormats` and selecting every field whose `SField::sMD_PseudoAccount`
* metadata bit is set. Currently includes `sfAMMID`, `sfVaultID`, and
* `sfLoanBrokerID`. The discovery is fully data-driven: adding a new
* pseudo-account type requires only tagging its key field with
* `SField::sMD_PseudoAccount` in `sfields.macro` — no manual registration
* here is needed.
*
* @return A const reference to the cached vector of pseudo-account fields.
* @note Non-active amendments are harmless: the corresponding field will
* never be set in practice, so the list remains correct regardless of
* which amendments are enabled.
*/
[[nodiscard]] std::vector<SField const*> const&
getPseudoAccountFields();
/** Returns true if and only if sleAcct is a pseudo-account or specific
pseudo-accounts in pseudoFieldFilter.
Returns false if sleAcct is:
- NOT a pseudo-account OR
- NOT a ltACCOUNT_ROOT OR
- null pointer
*/
/** Determine whether an SLE is a pseudo-account (optionally of a specific type).
*
* Returns `true` only when all three conditions hold: `sleAcct` is non-null,
* its ledger-entry type is `ltACCOUNT_ROOT`, and at least one pseudo-account
* designator field (from `getPseudoAccountFields()`) is present. When
* `pseudoFieldFilter` is non-empty, only fields in the filter are considered,
* allowing callers to distinguish AMM pseudo-accounts from Vault
* pseudo-accounts.
*
* @param sleAcct The SLE to inspect; may be null.
* @param pseudoFieldFilter Optional subset of pseudo-account fields to match
* against. An empty set (the default) matches any pseudo-account field.
* @return `true` if `sleAcct` is a pseudo-account (of a type in the filter
* when one is provided); `false` otherwise.
*/
[[nodiscard]] bool
isPseudoAccount(
std::shared_ptr<SLE const> sleAcct,
std::set<SField const*> const& pseudoFieldFilter = {});
/** Convenience overload that reads the account from the view. */
/** Convenience overload that looks up the account from a `ReadView`.
*
* Reads the `AccountRoot` for `accountId` via `keylet::account()` and
* delegates to the SLE overload.
*
* @param view The ledger view to query.
* @param accountId The account address to look up.
* @param pseudoFieldFilter Optional field filter forwarded to the SLE overload.
* @return `true` if the account exists and is a pseudo-account matching the
* filter; `false` otherwise.
*/
[[nodiscard]] inline bool
isPseudoAccount(
ReadView const& view,
@@ -90,22 +177,48 @@ isPseudoAccount(
return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter);
}
/**
* Create pseudo-account, storing pseudoOwnerKey into ownerField.
/** Create a protocol-owned pseudo-account `AccountRoot` SLE.
*
* The list of valid ownerField is maintained in AccountRootHelpers.cpp and
* the caller to this function must perform necessary amendment check(s)
* before using a field. The amendment check is **not** performed in
* createPseudoAccount.
* Derives a collision-free address via `pseudoAccountAddress()`, constructs
* an `AccountRoot` with zero balance, `lsfDisableMaster | lsfDefaultRipple |
* lsfDepositAuth`, and stores `pseudoOwnerKey` in `ownerField`. When
* `featureSingleAssetVault` or `featureLendingProtocol` is enabled,
* `sfSequence` is set to `0`; otherwise it is set to the current ledger
* sequence. The zero sequence makes pseudo-accounts visually distinguishable
* and provides an extra barrier against accidental transaction submission.
*
* In debug builds, an `XRPL_ASSERT` fires if `ownerField` does not carry the
* `SField::sMD_PseudoAccount` flag, catching misuse at development time.
*
* @param view The mutable ledger view into which the new SLE is
* inserted.
* @param pseudoOwnerKey The 256-bit key of the owning object (e.g., the AMM
* or Vault ledger entry key); stored in `ownerField` on the new SLE.
* @param ownerField The back-link field written on the new SLE; must be
* one of the fields returned by `getPseudoAccountFields()`.
* @return The newly created SLE on success, or `tecDUPLICATE` if all 256
* address derivation attempts collided.
* @note Amendment checks are the **caller's** responsibility. This function
* is amendment-neutral by design; callers such as `VaultCreate` and
* `LoanBrokerSet` must gate on the relevant feature flag before invoking.
*/
[[nodiscard]] Expected<std::shared_ptr<SLE>, TER>
createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField);
/** Checks the destination and tag.
- Checks that the SLE is not null.
- If the SLE requires a destination tag, checks that there is a tag.
*/
/** Validate a payment destination SLE and its destination-tag requirement.
*
* Returns `tecNO_DST` if `toSle` is null (the destination account does not
* exist), and `tecDST_TAG_NEEDED` if the destination has set
* `lsfRequireDestTag` but the transaction supplies no tag. Returns
* `tesSUCCESS` otherwise.
*
* @param toSle The destination account SLE; may be null.
* @param hasDestinationTag `true` if the transaction includes a destination
* tag field.
* @return `tecNO_DST`, `tecDST_TAG_NEEDED`, or `tesSUCCESS`.
* @note The ledger enforces the *presence* of a tag but never interprets its
* value; semantics (e.g., exchange user IDs) are opaque to the protocol.
*/
[[nodiscard]] TER
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);

View File

@@ -1,3 +1,17 @@
/** @file
* Central contract for credential and deposit pre-authorization logic.
*
* Included by every fund-transfer transactor (Payment, EscrowFinish,
* PaymentChannelClaim, VaultDeposit) that must honor destination-account
* access controls.
*
* Functions divide along the preclaim / doApply boundary:
* - `xrpl::credentials::*` — read-only checks safe to call from preclaim.
* - `xrpl::verifyDepositPreauth` / `xrpl::verifyValidDomain` — mutating
* counterparts that must be called from doApply when the corresponding
* preclaim function succeeds, so that expired credential objects are
* physically deleted from the ledger as a side effect.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,57 +27,225 @@
namespace xrpl {
namespace credentials {
// These function will be used by the code that use DepositPreauth / Credentials
// (and any future pre-authorization modes) as part of authorization (all the
// transfer funds transactions)
// Check if credential sfExpiration field has passed ledger's parentCloseTime
/** Test whether a credential SLE has passed its expiration time.
*
* Reads `sfExpiration` from @p sleCredential, defaulting to
* `std::numeric_limits<uint32_t>::max()` when the field is absent, so
* credentials with no expiration field never expire.
*
* @param sleCredential The credential SLE to inspect.
* @param closed The parent ledger's close time. Must be a
* NetClock epoch value — do not pass wall-clock time.
* @return `true` if the credential has expired, `false` otherwise.
*/
bool
checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
// Actually remove a credentials object from the ledger
/** Remove a credential SLE and its entries from both owner directories.
*
* A credential is indexed in two owner directories — the issuer's and the
* subject's. Reserve-count accounting depends on acceptance state:
* - Before acceptance (`lsfAccepted` unset): only the issuer holds the
* reserve; only the issuer's count is decremented.
* - After acceptance with distinct accounts: the subject holds the reserve
* and its count is decremented.
* - When issuer and subject are the same account, only one directory
* removal is performed.
*
* @note Paths indicating ledger corruption (missing account SLE, failed
* `dirRemove`) are marked `LCOV_EXCL` and are unreachable under normal
* operation.
*
* @param view Mutable ledger view through which the SLE is erased.
* @param sleCredential The credential SLE to delete; must not be null.
* @param j Journal for fatal-level error logging.
* @return `tesSUCCESS` on success; `tecNO_ENTRY` if @p sleCredential is
* null; `tecINTERNAL` or `tefBAD_LEDGER` on internal directory
* inconsistency.
*/
[[nodiscard]] TER
deleteSLE(ApplyView& view, std::shared_ptr<SLE> const& sleCredential, beast::Journal j);
// Amendment and parameters checks for sfCredentialIDs field
/** Validate the `sfCredentialIDs` field of a transaction at preflight time.
*
* Enforces non-empty, at most `kMAX_CREDENTIALS_ARRAY_SIZE` entries, and no
* duplicate hashes. Returns `tesSUCCESS` immediately when `sfCredentialIDs`
* is absent, as credentials are optional for most transaction types.
*
* @param tx The transaction under preflight validation.
* @param j Journal for trace-level malformed-transaction logging.
* @return `tesSUCCESS` if the field is absent or valid; `temMALFORMED` if
* the array is empty, too large, or contains duplicates.
*/
NotTEC
checkFields(STTx const& tx, beast::Journal j);
// Accessing the ledger to check if provided credentials are valid. Do not use
// in doApply (only in preclaim) since it does not remove expired credentials.
// If you call it in preclaim, you also must call verifyDepositPreauth in
// doApply
/** Verify that all credentials in a transaction exist, are owned by the
* sender, and have been accepted — for use in preclaim only.
*
* Checks each ID in `sfCredentialIDs`: the SLE must exist, its `sfSubject`
* must equal @p src, and `lsfAccepted` must be set. Expiration is
* deliberately not checked here; expired credentials are deleted in doApply
* by `verifyDepositPreauth` or `verifyValidDomain`.
*
* @note If this returns `tesSUCCESS` in preclaim, the caller must invoke
* `verifyDepositPreauth` in doApply to garbage-collect any credentials
* that expire before the enclosing transaction applies.
*
* @param tx The transaction whose `sfCredentialIDs` field is inspected.
* @param view Read-only ledger view for SLE lookups.
* @param src The account that must own every listed credential.
* @param j Journal for trace-level logging.
* @return `tesSUCCESS` if `sfCredentialIDs` is absent or all credentials are
* valid; `tecBAD_CREDENTIALS` if any credential is missing, belongs to a
* different account, or has not been accepted.
*/
TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
// Check if subject has any credential maching the given domain. If you call it
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
// doApply. This will ensure that expired credentials are deleted.
/** Check whether @p subject holds a live, accepted credential for a
* permissioned domain — for use in preclaim only.
*
* Reads the `PermissionedDomain` SLE, iterates its `sfAcceptedCredentials`
* array, and looks up the corresponding credential SLE for @p subject.
* A credential qualifies when it exists, has not expired, and carries
* `lsfAccepted`.
*
* Because a `ReadView` is immutable, expired credentials cannot be deleted
* here. The function returns `tecEXPIRED` when all matching credentials
* are expired — signaling the caller that the condition may resolve in
* doApply where `verifyValidDomain` will physically remove them.
*
* @note If this returns `tecEXPIRED` in preclaim, the caller must invoke
* `verifyValidDomain` in doApply so that expired objects are
* garbage-collected even if the transaction ultimately fails.
*
* @param view Read-only ledger view.
* @param domainID Key of the `PermissionedDomain` SLE to check against.
* @param subject Account that must hold a qualifying credential.
* @return `tesSUCCESS` if a live accepted credential exists; `tecEXPIRED`
* if only expired credentials were found; `tecNO_AUTH` if no matching
* credential exists; `tecOBJECT_NOT_FOUND` if the domain does not exist.
*/
TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
// This function is only called when we about to return tecNO_PERMISSION
// because all the checks for the DepositPreauth authorization failed.
/** Check whether a set of credential IDs matches a credential-set
* `DepositPreauth` entry for the destination account.
*
* Builds a sorted `std::set<std::pair<AccountID, Slice>>` of
* `(issuer, credentialType)` pairs from @p credIDs and tests for the
* existence of the corresponding `keylet::depositPreauth(dst, sorted)`.
* The sorted representation matches the canonical key used at
* `DepositPreauth` creation time.
*
* @note Credential existence is assumed to have been confirmed in preclaim.
* A missing SLE here indicates an internal consistency error.
* @note `Slice` members in the internal sorted set are non-owning views
* into SLE storage. A `lifeExtender` vector keeps the SLEs alive for
* the duration of the lookup.
*
* @param view Read-only ledger view for SLE and keylet lookups.
* @param credIDs The `sfCredentialIDs` vector from the transaction.
* @param dst The destination account whose `DepositPreauth` is checked.
* @return `tesSUCCESS` if a matching `DepositPreauth` object exists;
* `tecNO_PERMISSION` if none exists; `tefINTERNAL` if a credential SLE
* is unexpectedly missing or a duplicate pair is encountered.
*/
TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
// Sort credentials array, return empty set if there are duplicates
/** Build a sorted `(issuer, credentialType)` set from a credentials array.
*
* Produces the canonical representation used to key `DepositPreauth`
* objects. Each element of @p credentials must carry `sfIssuer` and
* `sfCredentialType`.
*
* @param credentials An `STArray` of credential pairs, as stored in a
* `DepositPreauth` or `PermissionedDomainSet` transaction.
* @return A sorted set of `(AccountID, Slice)` pairs; an empty set if any
* duplicate `(issuer, credentialType)` pair is detected.
*/
std::set<std::pair<AccountID, Slice>>
makeSorted(STArray const& credentials);
// Check credentials array passed to DepositPreauth/PermissionedDomainSet
// transactions
/** Validate a credential array in `DepositPreauth` or
* `PermissionedDomainSet` transactions at preflight time.
*
* Credentials in these transactions are `(issuer, credentialType)` pairs
* rather than object hashes. Enforces: non-empty; at most @p maxSize
* entries; valid issuer `AccountID`; `sfCredentialType` length in
* `[1, kMAX_CREDENTIAL_TYPE_LENGTH]` bytes; and no logical duplicates
* (detected via `sha512Half(issuer, credentialType)`).
*
* @param credentials The `STArray` of credential pairs to validate.
* @param maxSize Maximum permitted array length (caller-supplied per
* transaction type).
* @param j Journal for trace-level malformed-transaction logging.
* @return `tesSUCCESS` if all entries are valid; `temARRAY_EMPTY`,
* `temARRAY_TOO_LARGE`, `temINVALID_ACCOUNT_ID`, or `temMALFORMED`
* on the first constraint violation found.
*/
NotTEC
checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
} // namespace credentials
// Check expired credentials and for credentials maching DomainID of the ledger
// object
/** Enforce domain-credential authorization in doApply, deleting expired
* credentials as a side effect.
*
* The doApply counterpart to `credentials::validDomain`. Collects all
* credential SLEs for @p account that match the `sfAcceptedCredentials`
* list of the `PermissionedDomain` at @p domainID, calls
* `credentials::removeExpired` to physically delete any that have expired,
* then re-checks whether at least one live, accepted credential remains.
*
* The two-pass design (collect → expire → re-validate) ensures expired
* objects are garbage-collected even when the surrounding transaction
* ultimately fails.
*
* @param view Mutable ledger view; expired credential SLEs are erased.
* @param account Account whose credentials are being verified.
* @param domainID Key of the `PermissionedDomain` SLE.
* @param j Journal for trace/error logging.
* @return `tesSUCCESS` if a live accepted credential for the domain exists;
* `tecEXPIRED` if only expired credentials were found; `tecNO_PERMISSION`
* if no matching credential exists; `tecOBJECT_NOT_FOUND` if the domain
* SLE is missing; or a propagated `TER` error from `removeExpired` under
* `fixSecurity3_1_3`.
*/
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
// Check expired credentials and for existing DepositPreauth ledger object
/** Enforce deposit pre-authorization in doApply, deleting expired credentials
* as a side effect.
*
* Called by Payment, EscrowFinish, and PaymentChannelClaim when the
* destination account has `lsfDepositAuth` set. Authorization succeeds
* when any of the following hold:
* - `src == dst` (self-payments are always allowed).
* - `keylet::depositPreauth(dst, src)` exists (account-level pre-auth).
* - A credential-set `DepositPreauth` object exists for the credentials
* submitted via `sfCredentialIDs` (via `credentials::authorizedDepositPreauth`).
*
* If `sfCredentialIDs` is present, `credentials::removeExpired` is called
* unconditionally before the authorization tests. If any credential was
* expired, `tecEXPIRED` is returned immediately without attempting
* authorization.
*
* @param tx The transaction under doApply; may carry `sfCredentialIDs`.
* @param view Mutable ledger view; expired credential SLEs may be erased.
* @param src The sending account.
* @param dst The destination account.
* @param sleDst The destination account's SLE, used to test `lsfDepositAuth`.
* If null, `lsfDepositAuth` is treated as unset and the function returns
* `tesSUCCESS`.
* @param j Journal for trace/error logging.
* @return `tesSUCCESS` if authorized or `lsfDepositAuth` is not set;
* `tecEXPIRED` if submitted credentials have expired;
* `tecNO_PERMISSION` if no matching pre-authorization exists; or a
* propagated error from `removeExpired` or `authorizedDepositPreauth`.
*/
TER
verifyDepositPreauth(
STTx const& tx,

View File

@@ -1,3 +1,12 @@
/** @file
* Runtime enforcement helpers for the XRPL delegate account system.
*
* Transactors call these two functions in sequence during permission
* validation: `checkTxPermission` for the broad transaction-type gate,
* then `loadGranularPermission` when a more restrictive, field-level
* check is needed. The permission schema and encoding convention live
* in `xrpl/protocol/Permissions.h`.
*/
#pragma once
#include <xrpl/protocol/Permissions.h>
@@ -7,24 +16,64 @@
namespace xrpl {
/**
* Check if the delegate account has permission to execute the transaction.
* @param delegate The delegate account.
* @param tx The transaction that the delegate account intends to execute.
* @return tesSUCCESS if the transaction is allowed, terNO_DELEGATE_PERMISSION
* if not.
/** Determine whether a delegate relationship grants blanket permission for
* a transaction type.
*
* Scans the `sfPermissions` array of the `ltDELEGATE` ledger entry for an
* element whose `sfPermissionValue` equals `tx.getTxnType() + 1` — the
* transaction-level encoding used on-ledger. Returns `tesSUCCESS` on the
* first match, or `terNO_DELEGATE_PERMISSION` if no match is found.
*
* A null `delegate` pointer is treated as a missing ledger entry and
* returns `terNO_DELEGATE_PERMISSION` immediately.
*
* The result is `NotTEC` (no `tec` fee-claim codes) because the two
* meaningful outcomes are `tesSUCCESS` and `terNO_DELEGATE_PERMISSION`.
* The `ter` (retry) code is intentional: the `ltDELEGATE` object could be
* updated in a subsequent ledger, so an identical transaction may succeed
* in the future without modification.
*
* @param delegate Immutable `ltDELEGATE` SLE obtained via `view.read()`;
* may be null, in which case `terNO_DELEGATE_PERMISSION` is returned.
* @param tx The transaction whose type is being checked.
* @return `tesSUCCESS` if the delegate holds a transaction-level permission
* for `tx`'s type; `terNO_DELEGATE_PERMISSION` otherwise.
* @note Callers should resolve the SLE via `keylet::delegate(account,
* delegate)` and pass it directly. If the SLE is absent from the
* ledger, `view.read()` returns null and the guard here handles it.
* @see loadGranularPermission — for fine-grained per-flag enforcement when
* this function returns `terNO_DELEGATE_PERMISSION`.
*/
NotTEC
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
/**
* Load the granular permissions granted to the delegate account for the
* specified transaction type
* @param delegate The delegate account.
* @param type Used to determine which granted granular permissions to load,
* based on the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
/** Populate a set with all granular sub-operation permissions the delegate
* holds for a given transaction type.
*
* Walks the `sfPermissions` array of the `ltDELEGATE` ledger entry. For
* each element, it casts the `sfPermissionValue` to `GranularPermissionType`
* and asks `Permission::getInstance().getGranularTxType()` whether that
* granular type belongs to `type`. Matching values are inserted into
* `granularPermissions`.
*
* A null `delegate` pointer is a silent no-op; the output set is left
* unchanged.
*
* The set is caller-owned and passed by reference so transactors can declare
* it on the stack, avoiding heap allocation. Callers may also accumulate
* results from multiple calls if needed.
*
* @param delegate Immutable `ltDELEGATE` SLE; may be null (no-op).
* @param type The transaction type whose granular permissions should be
* collected (e.g., `ttTRUST_SET`, `ttPAYMENT`).
* @param granularPermissions Output set populated with every
* `GranularPermissionType` the delegate holds that maps to `type`.
* @note This function is the second stage of a two-step check. Call
* `checkTxPermission` first; only invoke this when that returns
* `terNO_DELEGATE_PERMISSION` and the transaction type supports
* granular flags. Calling it unconditionally wastes a full scan of
* the permissions array on the common case.
* @see checkTxPermission — for the broad transaction-type gate.
*/
void
loadGranularPermission(

View File

@@ -1,3 +1,22 @@
/** @file
* Traversal utilities for ledger directory nodes (`ltDIR_NODE`).
*
* A directory is a linked list of pages (`SLE` of type `ltDIR_NODE`),
* where each page holds an `sfIndexes` field (`STVector256`) of child
* ledger-entry keys and an `sfIndexNext` field that chains to the next
* page. Owner directories track every object an account holds; order-
* book directories track standing offers at a given quality.
*
* This header provides:
* - A const-aware template core (`detail::internalDirFirst` /
* `detail::internalDirNext`) that unifies the read and write traversal
* paths at compile time.
* - A deprecated step-iterator API (`cdirFirst`, `cdirNext`, `dirFirst`,
* `dirNext`) used only where cursor patching during deletion is required.
* - Higher-level callback iterators (`forEachItem`, `forEachItemAfter`)
* for exhaustive and paginated walks.
* - `dirIsEmpty` and `describeOwnerDir` utility helpers.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -15,6 +34,32 @@ namespace xrpl {
namespace detail {
/** Advance a directory cursor to the next entry, crossing page boundaries.
*
* When the cursor has consumed all entries in the current page, the function
* follows `sfIndexNext` to load the next page and tail-calls itself to yield
* the first entry of that page in a single logical step. If `sfIndexNext` is
* zero the directory is exhausted: `entry` is zeroed and `false` is returned.
*
* The `if constexpr` branch selects `view.read()` when `N` is `SLE const`
* (read-only traversal via `ReadView`) and `view.peek()` when `N` is `SLE`
* (mutable traversal via `ApplyView`), keeping both paths in one template.
*
* @tparam V A view type derived from `ReadView`.
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
* @param view The ledger view to query pages from.
* @param root The 256-bit key of the directory's root (anchor) page.
* @param page In/out: the current page SLE; updated when a page boundary
* is crossed.
* @param index In/out: the zero-based cursor within `page->sfIndexes`;
* incremented to point past the entry that was just returned.
* @param entry Out: the key of the current entry on success; zeroed on
* end-of-directory.
* @return `true` if an entry was produced; `false` if the directory is
* exhausted.
* @note An `XRPL_ASSERT` fires in instrumented builds if `index` exceeds
* the page's entry count, indicating a corrupted cursor.
*/
template <
class V,
class N,
@@ -64,6 +109,23 @@ internalDirNext(
return true;
}
/** Initialise a directory cursor at the first entry of the root page.
*
* Loads the root page via `view.read()` (when `N` is `SLE const`) or
* `view.peek()` (when `N` is `SLE`), resets the index to zero, then
* delegates to `internalDirNext` to yield the first entry.
*
* @tparam V A view type derived from `ReadView`.
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
* @param view The ledger view to query pages from.
* @param root The 256-bit key of the directory's root (anchor) page.
* @param page Out: set to the root page SLE on success; unchanged if the
* root page is absent.
* @param index Out: set to zero before delegating to `internalDirNext`.
* @param entry Out: the key of the first entry on success.
* @return `true` if the directory has at least one entry; `false` if the
* root page is absent or the directory is empty.
*/
template <
class V,
class N,
@@ -119,6 +181,24 @@ cdirFirst(
unsigned int& index,
uint256& entry);
/** Returns the first entry in the directory, advancing the index.
*
* Mutable overload of `cdirFirst` for use with `ApplyView`. Yields a
* `shared_ptr<SLE>` obtained via `view.peek()`, allowing the caller to
* modify the page SLE if required.
*
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
* code. Use this overload only when cursor patching during deletion
* is required (see `cleanupOnAccountDelete` in `View.cpp`).
*
* @param view The mutable view against which to operate.
* @param root The 256-bit key of the directory's root page.
* @param page Out: set to the root page SLE obtained via `peek()`.
* @param index Out: set to the cursor position within `page->sfIndexes`.
* @param entry Out: the key of the first directory entry.
* @return `true` if the directory has at least one entry; `false`
* otherwise.
*/
bool
dirFirst(
ApplyView& view,
@@ -151,6 +231,31 @@ cdirNext(
unsigned int& index,
uint256& entry);
/** Advances the mutable directory cursor to the next entry.
*
* Mutable overload of `cdirNext` for use with `ApplyView`. Page
* transitions are handled transparently: when `index` reaches the end
* of the current page, `sfIndexNext` is followed and the cursor is reset
* to the first entry of the new page.
*
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
* code. The primary use case for this function is cursor patching
* during deletion: `cleanupOnAccountDelete` (in `View.cpp`) decrements
* `index` after each deletion so the cursor stays aligned as entries
* shift — a technique that relies on the cursor being externally
* accessible.
*
* @param view The mutable view against which to operate.
* @param root The 256-bit key of the directory's root page.
* @param page In/out: the current page SLE; updated on page boundary
* crossing.
* @param index In/out: the cursor position within `page->sfIndexes`;
* incremented past the returned entry.
* @param entry Out: the key of the current entry on success; zeroed when
* the directory is exhausted.
* @return `true` if an entry was produced; `false` if the directory is
* exhausted.
*/
bool
dirNext(
ApplyView& view,
@@ -160,19 +265,61 @@ dirNext(
uint256& entry);
/** @} */
/** Iterate all items in the given directory. */
/** Exhaustively walk every entry in a directory, invoking a callback for each.
*
* Iterates all pages of the directory in `sfIndexNext` chain order, calling
* `f` with the materialised child SLE for every key in `sfIndexes`. The
* child SLE is obtained via `view.read(keylet::child(key))` and may be
* `nullptr` if the referenced entry is absent from the view; the callback
* must handle that case. Iteration terminates when `sfIndexNext` is zero or
* a page SLE is missing; there is no early-exit mechanism.
*
* @param view The read-only ledger view to query.
* @param root Keylet of the directory's root page; must have type
* `ltDIR_NODE`.
* @param f Callback invoked with each child SLE (possibly `nullptr`).
* @note An `XRPL_ASSERT` fires in instrumented builds if `root.type` is
* not `ltDIR_NODE`; in release builds the function returns silently.
*/
void
forEachItem(
ReadView const& view,
Keylet const& root,
std::function<void(std::shared_ptr<SLE const> const&)> const& f);
/** Iterate all items after an item in the given directory.
@param after The key of the item to start after
@param hint The directory page containing `after`
@param limit The maximum number of items to return
@return `false` if the iteration failed
*/
/** Paginated directory walk, delivering items that follow a cursor key.
*
* Supports cursor-based pagination as used by RPC handlers such as
* `account_offers`, `account_lines`, and `account_channels`. When
* `after` is non-zero the function first attempts to jump to the `hint`
* page (the page the client last saw) to avoid re-scanning all prior
* pages; if the hint does not contain `after`, it falls back to a linear
* scan from the root. Once the cursor is located, subsequent entries are
* delivered to `f` until `limit` is reached or the directory is exhausted.
*
* The callback `f` returns `bool`: `true` to continue (and decrement the
* limit counter), `false` to stop immediately regardless of the remaining
* limit. Callers conventionally request `limit + 1` items and infer a
* non-empty next page when exactly `limit + 1` items are delivered.
*
* @param view The read-only ledger view to query.
* @param root Keylet of the directory's root page; must have type
* `ltDIR_NODE`.
* @param after Cursor key: only entries that follow this key in directory
* order are delivered. Pass `uint256()` (zero) to start from the
* beginning, in which case the function always returns `true`.
* @param hint Page number expected to contain `after`; used as a fast-
* path optimisation. Ignored when `after` is zero or when the hint
* page does not actually contain `after`.
* @param limit Maximum number of `true`-returning callback invocations
* before the walk stops.
* @param f Callback invoked for each qualifying child SLE (possibly
* `nullptr` if the key is absent). Return `true` to continue
* iteration; `false` to stop early.
* @return `true` if `after` was found (or `after` is zero); `false` if
* the cursor key was never located, indicating a stale or invalid
* marker that callers should surface as a pagination error.
*/
bool
forEachItemAfter(
ReadView const& view,
@@ -182,7 +329,15 @@ forEachItemAfter(
unsigned int limit,
std::function<bool(std::shared_ptr<SLE const> const&)> const& f);
/** Iterate all items in an account's owner directory. */
/** Exhaustively walk every entry in an account's owner directory.
*
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
* forwards to `forEachItem(view, Keylet, f)`.
*
* @param view The read-only ledger view to query.
* @param id The account whose owner directory should be iterated.
* @param f Callback invoked with each child SLE (possibly `nullptr`).
*/
inline void
forEachItem(
ReadView const& view,
@@ -192,12 +347,22 @@ forEachItem(
forEachItem(view, keylet::ownerDir(id), f);
}
/** Iterate all items after an item in an owner directory.
@param after The key of the item to start after
@param hint The directory page containing `after`
@param limit The maximum number of items to return
@return `false` if the iteration failed
*/
/** Paginated walk of an account's owner directory after a cursor key.
*
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
* forwards to `forEachItemAfter(view, Keylet, after, hint, limit, f)`.
*
* @param view The read-only ledger view to query.
* @param id The account whose owner directory should be iterated.
* @param after Cursor key; pass `uint256()` (zero) to start from the
* beginning.
* @param hint Page number expected to contain `after`.
* @param limit Maximum number of `true`-returning callback invocations.
* @param f Callback invoked for each qualifying child SLE. Return `true`
* to continue; `false` to stop early.
* @return `true` if `after` was found (or is zero); `false` if the cursor
* was never located.
*/
inline bool
forEachItemAfter(
ReadView const& view,
@@ -210,13 +375,36 @@ forEachItemAfter(
return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f);
}
/** Returns `true` if the directory is empty
@param key The key of the directory
*/
/** Returns `true` if the directory contains no entries.
*
* An empty `sfIndexes` array on the root page is necessary but not
* sufficient: the root is an anchor page and may have an empty index
* while `sfIndexNext` still points to a populated subsequent page. Both
* conditions — empty `sfIndexes` *and* `sfIndexNext == 0` — must hold
* before declaring the directory empty. A missing root SLE is also
* treated as empty.
*
* @param view The read-only ledger view to query.
* @param k Keylet of the directory's root page.
* @return `true` if the directory has no entries or does not exist;
* `false` otherwise.
*/
[[nodiscard]] bool
dirIsEmpty(ReadView const& view, Keylet const& k);
/** Returns a function that sets the owner on a directory SLE */
/** Returns a callback that stamps a new directory page with its owner account.
*
* The returned `std::function<void(SLE::ref)>` sets `sfOwner = account` on
* the newly allocated `ltDIR_NODE` SLE. It is passed as the `describe`
* argument to `ApplyView::dirInsert` throughout the codebase (e.g.,
* `RippleStateHelpers.cpp`, `PaymentChannelCreate.cpp`) and is invoked only
* when `dirInsert` actually allocates a fresh overflow page, keeping the
* owning account ID out of the generic insertion logic.
*
* @param account The `AccountID` to record as `sfOwner` on each new page.
* @return A callable suitable for the `describe` parameter of
* `ApplyView::dirInsert`.
*/
[[nodiscard]] std::function<void(SLE::ref)>
describeOwnerDir(AccountID const& account);

View File

@@ -1,3 +1,13 @@
/** @file
* Token-delivery helper for IOU and MPT escrow resolution.
*
* Implements `escrowUnlockApplyHelper`, the single function responsible for
* crediting the appropriate account when an IOU or MPT escrow is finished
* (`EscrowFinish`) or cancelled (`EscrowCancel`) under `featureTokenEscrow`.
* The function is specialised once for `Issue` (IOU trust-line path) and once
* for `MPTIssue` (MPToken path); callers reach the correct specialisation via
* `std::visit` on the `Asset` variant, with zero runtime dispatch overhead.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,6 +23,33 @@
namespace xrpl {
/** Credit an account with tokens held in escrow, applying transfer-fee logic.
*
* Primary template — no body is provided. Only the `Issue` and `MPTIssue`
* full specialisations are defined. Callers should invoke via `std::visit`
* on an `Asset` variant so the compiler selects the correct specialisation
* at compile time.
*
* @tparam T Asset type; must satisfy `ValidIssueType` (`Issue` or `MPTIssue`).
* @param view Mutable ledger view on which state changes are applied.
* @param lockedRate Transfer rate snapshotted at escrow creation time.
* Pass `kPARITY_RATE` for cancellations (return to sender, no fee).
* @param sleDest SLE for the destination account (`receiver`); used for
* owner-count and reserve checks when auto-creating a trust line or
* MPToken holding object.
* @param xrpBalance Pre-fee XRP balance of the destination account; compared
* against the incremental reserve required to create a new holding object.
* @param amount Escrowed token amount (face value locked at escrow creation).
* @param issuer Token issuer.
* @param sender Escrow creator / original token sender.
* @param receiver Account that will receive the unlocked tokens.
* @param createAsset When `true`, auto-creates a trust line or MPToken object
* for `receiver` if one does not already exist. Callers set this only
* when the transaction submitter is also the beneficiary, preserving
* account sovereignty over directory entries.
* @param journal Logging sink.
* @return `tesSUCCESS` on success, or a `tec` error code on failure.
*/
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
@@ -27,6 +64,40 @@ escrowUnlockApplyHelper(
bool createAsset,
beast::Journal journal);
/** IOU trust-line specialisation of `escrowUnlockApplyHelper`.
*
* Delivers IOU tokens from a finished or cancelled escrow to `receiver`,
* optionally creating the trust line and applying the snapshotted transfer
* fee.
*
* **Issuer short-circuits.** `sender == issuer` returns `tecINTERNAL` (an
* issuer cannot be an escrow originator for their own obligation).
* `receiver == issuer` returns `tesSUCCESS` immediately — delivery to the
* issuer is a redemption handled by the calling transactor at the balance
* level.
*
* **Trust line creation.** When `createAsset` is `true` and no trust line
* exists, one is created with a zero balance and zero limit via `trustCreate`.
* The `sfDefaultRipple` flag is inherited from `sleDest`. Reserve is checked
* first; insufficient reserve returns `tecNO_LINE_INSUF_RESERVE`. When
* `createAsset` is `false` and no line exists, returns `tecNO_LINE`.
*
* **Transfer fee.** The effective rate is `min(lockedRate, currentRate)`,
* protecting the receiver from a rate increase during the escrow lifetime.
* The fee is deducted *from* `amount` (not added on top), so `receiver` gets
* `amount - fee`. When neither party is the issuer and the rate differs from
* `kPARITY_RATE`, the check against the trust-line limit uses `finalAmt`.
*
* **Limit check.** When `createAsset` is `false`, the post-transfer balance
* is compared to `receiver`'s trust-line limit; `tecLIMIT_EXCEEDED` is
* returned if the delivery would exceed it. This check is skipped when
* `createAsset` is `true` because a freshly created line has a zero limit
* and would always fail it spuriously.
*
* @note This function is reached via `std::visit` on an `Asset` variant in
* `EscrowFinish` and `EscrowCancel`. `EscrowCancel` always passes
* `kPARITY_RATE` so no fee is charged on the return-to-sender path.
*/
template <>
inline TER
escrowUnlockApplyHelper<Issue>(
@@ -70,21 +141,21 @@ escrowUnlockApplyHelper<Issue>(
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
(sleDest->getFlags() & lsfDefaultRipple) == 0, //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
journal); // journal
view,
recvLow,
issuer,
receiver,
trustLineKey.key,
sleDest,
false,
(sleDest->getFlags() & lsfDefaultRipple) == 0,
false,
false,
initialBalance,
Issue(currency, receiver),
0,
0,
journal);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
@@ -97,57 +168,43 @@ escrowUnlockApplyHelper<Issue>(
return tecNO_LINE;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
// Cap to the lower of the snapshotted and current rate to protect the receiver.
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
// Limit check skipped when createAsset is true (freshly created line has
// zero limit and would always fail spuriously).
if (!createAsset)
{
auto const sleRippleState = view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
// recvLow true → receiver is low side → use sfLowLimit; else sfHighLimit.
STAmount const lineLimit =
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!recvLow)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal);
@@ -157,6 +214,32 @@ escrowUnlockApplyHelper<Issue>(
return tesSUCCESS;
}
/** MPT specialisation of `escrowUnlockApplyHelper`.
*
* Delivers MPT tokens from a finished or cancelled escrow to `receiver`,
* optionally creating an MPToken holding object and applying the snapshotted
* transfer fee.
*
* **MPToken creation.** When `createAsset` is `true`, `receiver` is not the
* issuer, and no MPToken SLE exists for this issuance, one is created via
* `createMPToken` and the owner count is incremented. Insufficient reserve
* returns `tecINSUFFICIENT_RESERVE`. If no MPToken exists after the creation
* attempt (and `receiver` is not the issuer), returns `tecNO_PERMISSION`.
*
* **Transfer fee.** Identical to the `Issue` path: effective rate is
* `min(lockedRate, currentRate)`, fee is deducted *from* `amount`, and no
* fee is applied when either party is the issuer or the rate is parity.
*
* **`fixTokenEscrowV1` bug fix.** The gross amount passed to `unlockEscrowMPT`
* (used to reduce `sfOutstandingAmount`) is `amount` when the amendment is
* enabled, and `finalAmt` otherwise. Without the fix, the outstanding supply
* is only reduced by the net delivered amount, silently retaining the fee
* portion; with the fix, the full face value is removed from circulation and
* the fee is burned from the outstanding supply.
*
* @note `EscrowCancel` passes `kPARITY_RATE` so no fee is charged when
* tokens are returned to the original sender.
*/
template <>
inline TER
escrowUnlockApplyHelper<MPTIssue>(
@@ -189,7 +272,6 @@ escrowUnlockApplyHelper<MPTIssue>(
return ter; // LCOV_EXCL_LINE
}
// update owner count.
adjustOwnerCount(view, sleDest, 1, journal);
}
@@ -197,25 +279,16 @@ escrowUnlockApplyHelper<MPTIssue>(
return tecNO_PERMISSION;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
// Cap to the lower of the snapshotted and current rate to protect the receiver.
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
return unlockEscrowMPT(

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,16 @@
/** @file
* MPT-specific ledger helper declarations.
*
* Declares the MPT counterpart to `RippleStateHelpers.h`. The asset-agnostic
* `TokenHelpers.h` dispatchers route `MPTIssue`-typed calls here via
* `std::visit` on the `Asset` variant. In addition to the functions that
* mirror IOU trust-line semantics (freeze, transfer rate, holding lifecycle,
* authorization), this header exposes operations with no IOU equivalent:
* escrow accounting, DEX permission gating, supply-overflow arithmetic, and
* the two-phase authorization protocol specific to MPT.
*
* @see RippleStateHelpers.h, TokenHelpers.h
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -20,15 +33,65 @@ namespace xrpl {
//
//------------------------------------------------------------------------------
/** Check whether an entire MPT issuance is globally frozen.
*
* Reads the `MPTokenIssuance` SLE and tests `lsfMPTLocked`. A missing
* issuance SLE is treated as unfrozen.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance to check.
* @return `true` if `lsfMPTLocked` is set on the issuance; `false` otherwise.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
/** Check whether a specific account's MPToken holding is individually frozen.
*
* Reads the per-holder `MPToken` SLE and tests `lsfMPTLocked`. Returns
* `false` if no `MPToken` SLE exists for the account (i.e., the account
* holds no balance for this issuance).
*
* @param view The ledger state to query.
* @param account The account whose holding is checked.
* @param mptIssue The MPT issuance to check against.
* @return `true` if the account's `MPToken` carries `lsfMPTLocked`;
* `false` otherwise.
*/
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Check whether an account's access to an MPT issuance is frozen by any tier.
*
* Applies three checks in order: global issuance lock (`isGlobalFrozen`),
* per-account holding lock (`isIndividualFrozen`), and vault pseudo-account
* freeze (`isVaultPseudoAccountFrozen`). Short-circuits on the first match.
*
* @param view The ledger state to query.
* @param account The account to check.
* @param mptIssue The MPT issuance to check against.
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`;
* bounds pathological nested-vault configurations (currently unreachable
* in practice, but defended against up to `maxAssetCheckDepth`).
* @return `true` if any freeze tier applies; `false` otherwise.
*/
[[nodiscard]] bool
isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth = 0);
/** Check whether any account in a set is frozen for an MPT issuance.
*
* Sequences checks across separate passes to minimize cost: the global freeze
* is tested once and short-circuits immediately; individual per-account locks
* are checked for every account before the more expensive vault
* pseudo-account recursion begins.
*
* @param view The ledger state to query.
* @param accounts The set of accounts to check.
* @param mptIssue The MPT issuance to check against.
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`.
* @return `true` if the global freeze is set, or if any account carries an
* individual freeze, or if any account is a frozen vault pseudo-account;
* `false` otherwise.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
@@ -42,10 +105,18 @@ isAnyFrozen(
//
//------------------------------------------------------------------------------
/** Returns MPT transfer fee as Rate. Rate specifies
* the fee as fractions of 1 billion. For example, 1% transfer rate
* is represented as 1,010,000,000.
* @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object
/** Convert the `sfTransferFee` field of an MPT issuance to the XRPL `Rate` type.
*
* `sfTransferFee` is a `uint16` in the range 050,000 representing 050%
* (units of 0.001%). The encoding maps to `Rate` via
* `1,000,000,000 + (10,000 × fee)`, so a 50,000 field value becomes
* `1,500,000,000` (50% surcharge over the gross). When `sfTransferFee` is
* absent, `parityRate` (1,000,000,000 — no fee) is returned.
*
* @param view The ledger state to query.
* @param issuanceID The `MPTokenIssuanceID` of the issuance.
* @return The transfer rate as a `Rate` value; `parityRate` when no fee is
* configured or the issuance SLE is absent.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, MPTID const& issuanceID);
@@ -56,6 +127,18 @@ transferRate(ReadView const& view, MPTID const& issuanceID);
//
//------------------------------------------------------------------------------
/** Read-only pre-check: verify that an independent holding can be created.
*
* Validates two preconditions before `addEmptyHolding` mutates the ledger:
* the `MPTokenIssuance` must exist, and it must carry `lsfMPTCanTransfer`.
* Tokens without `lsfMPTCanTransfer` can only move directly between the
* issuer and counterparties, making independent holdings meaningless.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance the caller wants to hold.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance SLE is absent,
* or `tecNO_AUTH` if `lsfMPTCanTransfer` is not set.
*/
[[nodiscard]] TER
canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
@@ -65,6 +148,33 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
//
//------------------------------------------------------------------------------
/** Core MPToken SLE lifecycle function — create, delete, or toggle authorization.
*
* Behavior depends on `holderID`:
* - `holderID` absent (`nullopt`): `account` is the holder. Without
* `tfMPTUnauthorize`, a new zero-balance `MPToken` SLE is created and
* inserted into the owner directory; the XRP reserve is enforced when
* `ownerCount >= 2` (same policy as trust lines). With `tfMPTUnauthorize`,
* the existing SLE is erased and the owner count decremented.
* - `holderID` set: `account` must be the issuance's issuer. The function
* toggles `lsfMPTAuthorized` on the holder's existing `MPToken` SLE.
*
* @param view The mutable ledger state.
* @param priorBalance XRP balance before this transaction; used only for the
* reserve check when creating a new holding (`holderID` absent and
* `tfMPTUnauthorize` not set).
* @param mptIssuanceID The issuance being authorized or deauthorized.
* @param account Submitting account: the holder (when `holderID` is absent)
* or the issuer (when `holderID` is set).
* @param journal Logging sink.
* @param flags Transaction flags; `tfMPTUnauthorize` selects the
* delete/deauthorize path.
* @param holderID When set, `account` is the issuer and this is the holder
* whose `lsfMPTAuthorized` flag is toggled.
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE` if reserves are too low,
* `tecDUPLICATE` if the holding already exists, or a `tef` code on
* invariant violations.
*/
[[nodiscard]] TER
authorizeMPToken(
ApplyView& view,
@@ -75,12 +185,31 @@ authorizeMPToken(
std::uint32_t flags = 0,
std::optional<AccountID> holderID = std::nullopt);
/** Check if the account lacks required authorization for MPT.
/** Preclaim (read-only) authorization check for an MPT holding.
*
* requireAuth check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
* WeakAuth intentionally allows missing MPTokens under MPToken V2.
* Issuers are always authorized. When `featureSingleAssetVault` is active,
* vault and `LoanBroker` pseudo-accounts are implicitly authorized, and the
* check recurses into the vault's underlying asset (bounded by `depth`
* vs. `kMAX_ASSET_CHECK_DEPTH`). Domain-based authorization via
* `credentials::validDomain` takes precedence over `lsfMPTAuthorized` when
* `sfDomainID` is present on the issuance — a passing domain check succeeds
* even if no `MPToken` SLE exists.
*
* `WeakAuth` intentionally permits a missing `MPToken` SLE; used in MPToken
* V2 flows where the SLE is created on demand during apply.
*
* @note The recursion through vault assets is purely defensive; the ledger
* does not currently permit nested-vault MPT configurations.
* @param view The ledger state to query (read-only; called in preclaim).
* @param mptIssue The MPT issuance being accessed.
* @param account The account requesting access.
* @param authType Controls leniency toward missing `MPToken` SLEs;
* `WeakAuth` allows a missing SLE, `StrongAuth`/`Legacy` require it.
* @param depth Current recursion depth; guards against theoretical infinite
* recursion through nested vault configurations.
* @return `tesSUCCESS` if authorized, `tecOBJECT_NOT_FOUND` if the issuance
* is absent, `tecNO_AUTH` if authorization fails, or `tecEXPIRED` if
* domain credentials have expired.
*/
[[nodiscard]] TER
requireAuth(
@@ -90,11 +219,25 @@ requireAuth(
AuthType authType = AuthType::Legacy,
int depth = 0);
/** Enforce account has MPToken to match its authorization.
/** Enforce account has MPToken to match its authorization (doApply phase).
*
* Called from doApply - it will check for expired (and delete if found any)
* credentials matching DomainID set in MPTokenIssuance. Must be called if
* requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim.
* Must be called when `requireAuth` returned `tesSUCCESS` or `tecEXPIRED`
* during preclaim. Re-checks authorization and, if a `sfDomainID` is set on
* the issuance, runs `verifyValidDomain` (which deletes expired credentials
* as a side effect). When domain authorization succeeds but the account has
* no `MPToken` SLE, one is created on the fly using `priorBalance` for the
* XRP reserve check.
*
* @note Must not be called for the issuer account.
* @param view The mutable ledger state (called in doApply).
* @param mptIssuanceID The issuance being accessed.
* @param account The holder account; must not be the issuer.
* @param priorBalance XRP balance before this transaction; used when lazily
* allocating a new `MPToken` SLE for domain-authorized holders.
* @param j Logging sink.
* @return `tesSUCCESS`, `tecNO_AUTH` if not authorized, `tecEXPIRED` if
* credentials have expired, or `tecINSUFFICIENT_RESERVE` if the reserve
* check fails during on-demand SLE creation.
*/
[[nodiscard]] TER
enforceMPTokenAuthorization(
@@ -104,9 +247,20 @@ enforceMPTokenAuthorization(
XRPAmount const& priorBalance,
beast::Journal j);
/** Check if the destination account is allowed
* to receive MPT. Return tecNO_AUTH if it doesn't
* and tesSUCCESS otherwise.
/** Check whether a transfer between two accounts is permitted by the issuance.
*
* When `lsfMPTCanTransfer` is absent, third-party transfers are blocked.
* Transfers where either `from` or `to` is the issuer are always allowed,
* mirroring the IOU trust-line policy that lets issuers send and receive
* their own tokens unconditionally.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance involved in the transfer.
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, `tecOBJECT_NOT_FOUND`
* if the issuance SLE is absent, or `tecNO_AUTH` if `lsfMPTCanTransfer`
* is unset and neither endpoint is the issuer.
*/
[[nodiscard]] TER
canTransfer(
@@ -115,8 +269,16 @@ canTransfer(
AccountID const& from,
AccountID const& to);
/** Check if Asset can be traded on DEX. return tecNO_PERMISSION
* if it doesn't and tesSUCCESS otherwise.
/** Check whether an asset may be traded on the DEX.
*
* Dispatches via `asset.visit`: XRP and IOU assets always succeed; for MPT,
* reads the issuance SLE and checks `lsfMPTCanTrade`.
*
* @param view The ledger state to query.
* @param asset The asset to check; non-MPT assets always pass.
* @return `tesSUCCESS` if trading is permitted, `tecOBJECT_NOT_FOUND` if
* the MPT issuance SLE is absent, or `tecNO_PERMISSION` if
* `lsfMPTCanTrade` is not set.
*/
[[nodiscard]] TER
canTrade(ReadView const& view, Asset const& asset);
@@ -127,6 +289,24 @@ canTrade(ReadView const& view, Asset const& asset);
//
//------------------------------------------------------------------------------
/** Create a zero-balance `MPToken` holding for `accountID`.
*
* Short-circuits to `tesSUCCESS` when the caller is the issuer — issuers
* never hold a `MPToken` SLE for their own issuance. For all other accounts,
* delegates to `authorizeMPToken`, which enforces the XRP reserve requirement
* and inserts the SLE into the owner directory. Returns `tefINTERNAL` if the
* issuance SLE is missing or globally locked (invariant violations).
*
* @param view The mutable ledger state.
* @param accountID The account requesting the holding.
* @param priorBalance XRP balance before this transaction; forwarded to
* `authorizeMPToken` for the reserve check.
* @param mptIssue The MPT issuance to hold.
* @param journal Logging sink.
* @return `tesSUCCESS`, `tecDUPLICATE` if a holding already exists,
* `tecINSUFFICIENT_RESERVE` if reserves are too low, or `tefINTERNAL`
* on issuance-state invariant violations.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -135,6 +315,23 @@ addEmptyHolding(
MPTIssue const& mptIssue,
beast::Journal journal);
/** Delete a zero-balance `MPToken` holding.
*
* Requires `sfMPTAmount` to be zero and, when `fixSecurity3_1_3` is enabled,
* `sfLockedAmount` to be zero as well; returns `tecHAS_OBLIGATIONS` otherwise.
* When `accountID` is the issuer and no `MPToken` SLE exists, returns
* `tesSUCCESS` immediately — the normal issuer state. Otherwise delegates to
* `authorizeMPToken` with `tfMPTUnauthorize` to erase the SLE and decrement
* the owner count.
*
* @param view The mutable ledger state.
* @param accountID The account whose holding is being removed.
* @param mptIssue The MPT issuance.
* @param journal Logging sink.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if no holding exists (and
* caller is not the issuer), or `tecHAS_OBLIGATIONS` if the holding
* carries a non-zero balance or locked amount.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -148,6 +345,22 @@ removeEmptyHolding(
//
//------------------------------------------------------------------------------
/** Move MPT funds from a holder's spendable balance into escrow.
*
* Decrements `sfMPTAmount` and increments `sfLockedAmount` on the sender's
* `MPToken` SLE, then increments `sfLockedAmount` on the `MPTokenIssuance`
* SLE. `sfOutstandingAmount` on the issuance is deliberately left unchanged —
* escrowed tokens remain outstanding until the escrow completes and the
* recipient actually receives them. All arithmetic is guarded by
* `canSubtract`/`canAdd`.
*
* @param view The mutable ledger state.
* @param uGrantorID The account placing tokens in escrow; must not be the issuer.
* @param saAmount The MPT amount to lock; must be a valid `MPTIssue` amount.
* @param j Logging sink.
* @return `tesSUCCESS`, or a `tec`/`tef` error if the issuance or `MPToken`
* SLE is missing, the sender is the issuer, or an arithmetic guard fires.
*/
TER
lockEscrowMPT(
ApplyView& view,
@@ -155,6 +368,28 @@ lockEscrowMPT(
STAmount const& saAmount,
beast::Journal j);
/** Release MPT funds from escrow and credit the recipient.
*
* Decrements `sfLockedAmount` on both the sender's `MPToken` SLE and the
* `MPTokenIssuance` SLE by `grossAmount`. Then, depending on the receiver:
* - Receiver is a third party: `sfMPTAmount` on the receiver's `MPToken` is
* incremented by `netAmount`.
* - Receiver is the issuer: `sfOutstandingAmount` on the issuance is
* decremented by `netAmount` — tokens return to the issuer and retire.
* When `fixTokenEscrowV1` is enabled and `grossAmount > netAmount`, the fee
* difference is additionally subtracted from `sfOutstandingAmount` because
* the fee tokens are effectively burned. All arithmetic is guarded by
* `canSubtract`/`canAdd`.
*
* @param view The mutable ledger state.
* @param uGrantorID The escrow grantor; must not be the issuer.
* @param uGranteeID The escrow grantee (may be the issuer).
* @param netAmount The MPT amount credited to the receiver after fees.
* @param grossAmount The MPT amount unlocked from escrow (>= `netAmount`).
* @param j Logging sink.
* @return `tesSUCCESS`, or a `tec`/`tef` error on missing SLEs or
* arithmetic guard failure.
*/
TER
unlockEscrowMPT(
ApplyView& view,
@@ -164,6 +399,18 @@ unlockEscrowMPT(
STAmount const& grossAmount,
beast::Journal j);
/** Low-level primitive: insert a new `MPToken` SLE and link it into the owner directory.
*
* Inserts the SLE unconditionally without checking for duplicates, enforcing
* reserves, or verifying issuance validity. Callers must perform those checks
* before invoking this function.
*
* @param view The mutable ledger state.
* @param mptIssuanceID The issuance the token belongs to.
* @param account The account that will own the `MPToken`.
* @param flags Initial `sfFlags` value for the new SLE.
* @return `tesSUCCESS`, or `tecDIR_FULL` if the owner directory is full.
*/
TER
createMPToken(
ApplyView& view,
@@ -171,6 +418,21 @@ createMPToken(
AccountID const& account,
std::uint32_t const flags);
/** Idempotently ensure a `MPToken` holding exists for `holder`.
*
* Succeeds immediately if `holder` is the issuer or if the `MPToken` SLE
* already exists. Otherwise calls `createMPToken` and increments the owner
* count. Suitable for apply-phase callers that need to auto-create a holding
* without the full reserve and issuance validity checks performed by
* `addEmptyHolding`.
*
* @param view The mutable ledger state.
* @param mptIssue The MPT issuance the holder will hold.
* @param holder The account to receive the holding.
* @param j Logging sink.
* @return `tesSUCCESS`, `tecDIR_FULL` if the owner directory is full, or
* `tecINTERNAL` if the holder's account SLE is missing.
*/
TER
checkCreateMPT(
xrpl::ApplyView& view,
@@ -184,25 +446,62 @@ checkCreateMPT(
//
//------------------------------------------------------------------------------
// MaximumAmount doesn't exceed 2**63-1
/** Return the configured supply cap for an MPT issuance.
*
* Returns `sfMaximumAmount` when present, or `kMAX_MP_TOKEN_AMOUNT` (2^631)
* when the field is absent, representing an uncapped issuance. The result is
* always non-negative and fits in a `std::int64_t`.
*
* @param sleIssuance The `MPTokenIssuance` SLE to query.
* @return The maximum allowed outstanding amount.
*/
std::int64_t
maxMPTAmount(SLE const& sleIssuance);
// OutstandingAmount may overflow and available amount might be negative.
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
/** Compute remaining issuance headroom from a pre-read SLE.
*
* Returns `maxMPTAmount(sleIssuance) - sfOutstandingAmount`. May transiently
* be negative when the payment engine is processing a path step that
* temporarily exceeds `MaximumAmount` under `AllowMPTOverflow::Yes`.
*
* @param sleIssuance The `MPTokenIssuance` SLE to query.
* @return Headroom as a signed 64-bit integer; may be negative.
*/
std::int64_t
availableMPTAmount(SLE const& sleIssuance);
/** Compute remaining issuance headroom by reading the SLE from the view.
*
* Convenience overload that performs the SLE lookup. Throws
* `std::runtime_error` if the issuance SLE is absent — a missing issuance at
* this call site indicates a ledger consistency failure rather than a user
* error.
*
* @param view The ledger state to query.
* @param mptID The `MPTID` of the issuance.
* @return Headroom as a signed 64-bit integer; may be negative.
* @throws std::runtime_error if the `MPTokenIssuance` SLE is absent.
*/
std::int64_t
availableMPTAmount(ReadView const& view, MPTID const& mptID);
/** Checks for two types of OutstandingAmount overflow during a send operation.
* 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when
* `OutstandingAmount > MaximumAmount`. This threshold is used for direct
* directSendNoFee transactions that bypass the payment engine.
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
* for `accountSend` and payments processed via the payment engine.
/** Check whether crediting `sendAmount` would overflow the outstanding supply.
*
* Two distinct overflow thresholds are applied based on `allowOverflow`:
* 1. **`AllowMPTOverflow::No` (direct send):** Enforces the strict cap
* `OutstandingAmount + sendAmount ≤ MaximumAmount`. Used by
* `directSendNoFee` transactions that bypass the payment engine.
* 2. **`AllowMPTOverflow::Yes` (payment engine):** Raises the effective
* ceiling to `UINT64_MAX` to allow transient in-flight values that exceed
* `MaximumAmount` during path routing. A matching redemption step in the
* same transaction collapses the overshoot before settlement.
*
* @param sendAmount The proposed additional issuance; must be non-negative.
* @param outstandingAmount Current `sfOutstandingAmount` from the issuance SLE.
* @param maximumAmount The configured cap (`sfMaximumAmount` or
* `kMAX_MP_TOKEN_AMOUNT`).
* @param allowOverflow Selects which ceiling to apply.
* @return `true` if adding `sendAmount` would exceed the applicable limit.
*/
bool
isMPTOverflow(
@@ -211,18 +510,33 @@ isMPTOverflow(
std::int64_t maximumAmount,
AllowMPTOverflow allowOverflow);
/**
* Determine funds available for an issuer to sell in an issuer owned offer.
* Issuing step, which could be either MPTEndPointStep last step or BookStep's
* TakerPays may overflow OutstandingAmount. Redeeming step, in BookStep's
* TakerGets redeems the offer's owner funds, essentially balancing out
* the overflow, unless the offer's owner is the issuer.
/** Determine funds available for an issuer to sell in an issuer-owned DEX offer.
*
* During an issuing step (outbound from the issuer), the issuer's
* "available" balance is the remaining issuance headroom (`availableMPTAmount`)
* adjusted by `balanceHookSelfIssueMPT` to account for any amount already
* sold within the same payment. Without this hook, offer-crossing could
* allow the issuer to exceed `sfMaximumAmount` across parallel paths in the
* same transaction.
*
* @param view The ledger state to query.
* @param issue The MPT issuance for which to compute issuer funds.
* @return The effective amount the issuer can sell; zero if the issuance SLE
* is absent.
*/
[[nodiscard]] STAmount
issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* See ApplyView::issuerSelfDebitHookMPT().
/** Track MPT sold by an issuer that owns an MPT sell offer.
*
* Records the cumulative amount sold during the current payment step so that
* subsequent calls to `issuerFundsToSelfIssue` return a correctly reduced
* available balance. Delegates to `ApplyView::issuerSelfDebitHookMPT` after
* computing the current issuance headroom.
*
* @param view The mutable ledger state.
* @param issue The MPT issuance being sold.
* @param amount The additional amount sold in this step.
*/
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
@@ -233,9 +547,26 @@ issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amo
//
//------------------------------------------------------------------------------
/* Return true if a transaction is allowed for the specified MPT/account. The
* function checks MPTokenIssuance and MPToken objects flags to determine if the
* transaction is allowed.
/** Comprehensive MPT transaction permission check for DEX and payment types.
*
* Verifies in order: the issuer account exists, the `MPTokenIssuance` SLE
* exists, the issuance is not globally locked (`lsfMPTLocked`), the
* `lsfMPTCanTrade` flag is set, and — for non-issuer accounts — that
* `lsfMPTCanTransfer` is set and the account's own `MPToken` is not
* individually locked. A missing `MPToken` SLE for a non-issuer is treated
* as passing: some transaction types create the `MPToken` on demand and
* perform their own missing-token checks.
*
* @note Must not be called with `txType == ttPAYMENT`; use the payment-engine
* path's own checks for payments.
* @param v The ledger state to query.
* @param tx The transaction type being gated.
* @param asset The asset involved; non-MPT assets always succeed.
* @param accountID The account initiating the transaction.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance is absent,
* `tecNO_ISSUER` if the issuer account is gone, `tecLOCKED` if the
* issuance or account is frozen, or `tecNO_PERMISSION` if trading or
* transfer is not permitted.
*/
TER
checkMPTTxAllowed(ReadView const& v, TxType tx, Asset const& asset, AccountID const& accountID);

View File

@@ -1,3 +1,20 @@
/**
* @file NFTokenHelpers.h
* @brief Core helpers for NFT paged-directory and offer management.
*
* Declares all mutable and read-only operations on the NFToken paged-directory
* structure and offer queues. Every transaction that touches an NFToken —
* minting, burning, transferring, or creating/cancelling offers — calls these
* helpers rather than manipulating ledger state directly.
*
* @note NFTs are packed into doubly-linked `ltNFTOKEN_PAGE` SLEs, each
* holding up to `kDIR_MAX_TOKENS_PER_PAGE` (32) tokens sorted by
* `compareTokens()`. Tokens sharing the same low-96-bit masked value
* (issuer + taxon) are *equivalent* and must be collocated on the same
* page. Page key invariant: every token's low 96 bits are strictly less
* than the low 96 bits of its enclosing page key.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,18 +30,48 @@
namespace xrpl::nft {
/** Delete up to a specified number of offers from the specified token offer
* directory. */
* directory.
*
* Iterates the directory page-by-page, deleting offers in reverse index order
* within each page. Reverse iteration is required because `sfIndexes` is
* vector-backed and forward deletion would corrupt the remaining indices.
* Stops as soon as `maxDeletableOffers` offers have been removed.
*
* @param view The apply view to mutate.
* @param directory Keylet of the NFT buy or sell offer directory to drain.
* @param maxDeletableOffers Maximum number of offers to remove in this call.
* @return The number of offers actually deleted.
* @note Returns 0 immediately if `maxDeletableOffers` is 0. Used by
* `NFTokenBurn` to drain open offers within the per-transaction
* deletion cap (`maxDeletableTokenOfferEntries`).
*/
std::size_t
removeTokenOffersWithLimit(
ApplyView& view,
Keylet const& directory,
std::size_t maxDeletableOffers);
/** Finds the specified token in the owner's token directory. */
/** Finds the specified token in the owner's token directory.
*
* Read-only traversal: locates the `ltNFTOKEN_PAGE` candidate via `succ()`
* and searches the page's `sfNFTokens` array for a matching `sfNFTokenID`.
*
* @param view The read-only view to query.
* @param owner The account whose NFT directory is searched.
* @param nftokenID The 256-bit NFT identifier to look up.
* @return The matching token `STObject`, or `std::nullopt` if not found.
* @see findTokenAndPage for the mutable overload that also returns the page.
*/
std::optional<STObject>
findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID);
/** Finds the token in the owner's token directory. Returns token and page. */
/** Token and its containing page, returned by `findTokenAndPage()`.
*
* Bundles the located token `STObject` with the mutable `shared_ptr<SLE>`
* page so callers can modify the token in place without a second ledger
* traversal. The page pointer must be used exclusively on the same
* `ApplyView` that produced it.
*/
struct TokenAndPage
{
STObject token;
@@ -35,17 +82,81 @@ struct TokenAndPage
{
}
};
/** Finds the token in the owner's token directory and returns it with its page.
*
* Mutable traversal via `ApplyView::peek()`. Returns both the token
* `STObject` and the `shared_ptr<SLE>` page so that callers such as
* `NFTokenAcceptOffer` can pass the page directly to `removeToken()`,
* avoiding a redundant page lookup.
*
* @param view The apply view to query (mutable; uses `peek()`).
* @param owner The account whose NFT directory is searched.
* @param nftokenID The 256-bit NFT identifier to look up.
* @return A `TokenAndPage` containing the token and its page, or
* `std::nullopt` if the token is not found.
* @see findToken for the read-only alternative that returns only the token.
*/
std::optional<TokenAndPage>
findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
/** Insert the token in the owner's token directory. */
/** Insert the token in the owner's token directory.
*
* Locates or creates the appropriate `ltNFTOKEN_PAGE` via `getPageForToken()`.
* If the target page is full, it is split to make room; each split increments
* the owner's reserve count. Tokens are kept sorted within a page by
* `compareTokens()` (low 96-bit key first, full ID as tiebreaker).
*
* @param view The apply view to mutate.
* @param owner The account that will own the token.
* @param nft The token `STObject` to insert; must contain `sfNFTokenID`.
* @return `tesSUCCESS` on success, or `tecNO_SUITABLE_NFTOKEN_PAGE` if the
* target page is entirely filled with equivalent tokens (same low 96-bit
* key) and no split is possible.
*/
TER
insertToken(ApplyView& view, AccountID owner, STObject&& nft);
/** Remove the token from the owner's token directory. */
/** Remove the token from the owner's token directory.
*
* Page-discovery overload: locates the containing `ltNFTOKEN_PAGE` via
* `succ()` and then delegates to the two-argument form. Use this when
* the caller does not already hold a page reference.
*
* After erasure, attempts to merge the affected page with its neighbours;
* each successful merge credits one reserve. If the page becomes empty it
* is unlinked and erased.
*
* @param view The apply view to mutate.
* @param owner The account that currently holds the token.
* @param nftokenID The 256-bit NFT identifier to remove.
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the page or token cannot be
* found.
* @see removeToken(ApplyView&, AccountID const&, uint256 const&, shared_ptr<SLE> const&)
* for the overload that skips the page lookup.
*/
TER
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
/** Remove the token from the owner's token directory using a pre-located page.
*
* Caller-supplied page overload: skips the `succ()`-based page lookup when
* the caller already holds the page (e.g., from `findTokenAndPage()`).
* The `page` pointer must have been obtained from the same `ApplyView`
* instance.
*
* Under the `fixNFTokenPageLinks` amendment, if the emptied page is the final
* anchor page (`nftpage_max`), its contents are replaced with those of the
* previous page and the now-empty previous page is erased, preserving the
* invariant that the last page always has the stable sentinel key.
*
* @param view The apply view to mutate.
* @param owner The account that currently holds the token.
* @param nftokenID The 256-bit NFT identifier to remove.
* @param page The mutable SLE page known to contain the token.
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the token is not found on the
* supplied page.
*/
TER
removeToken(
ApplyView& view,
@@ -53,28 +164,74 @@ removeToken(
uint256 const& nftokenID,
std::shared_ptr<SLE> const& page);
/** Deletes the given token offer.
An offer is tracked in two separate places:
- The token's 'buy' directory, if it's a buy offer; or
- The token's 'sell' directory, if it's a sell offer; and
- The owner directory of the account that placed the offer.
The offer also consumes one incremental reserve.
/** Deletes the given token offer and removes it from both tracking directories.
*
* An offer is tracked in two separate places:
* - The token's `nft_buys` directory, if it is a buy offer; or
* - The token's `nft_sells` directory, if it is a sell offer; and
* - The owner's owner directory.
*
* Both directory entries are removed, the owner's reserve count is
* decremented by one, and the offer SLE is erased.
*
* @param view The apply view to mutate.
* @param offer The SLE for the offer to delete; must be of type
* `ltNFTOKEN_OFFER`.
* @return `true` if the offer was successfully deleted; `false` if the SLE
* is not of type `ltNFTOKEN_OFFER` or if a directory removal fails,
* acting as a type-safety guard.
*/
bool
deleteTokenOffer(ApplyView& view, std::shared_ptr<SLE> const& offer);
/** Repairs the links in an NFTokenPage directory.
Returns true if a repair took place, otherwise false.
*/
/** Repairs the links in an NFToken page directory.
*
* Walks the entire `ltNFTOKEN_PAGE` chain for the owner and corrects any
* broken `sfNextPageMin` / `sfPreviousPageMin` links. If the final page does
* not have the expected `nftpage_max` sentinel key, its contents are migrated
* to a newly created SLE with the correct key, the old SLE is erased, and the
* chain is relinked. Owner count is unchanged by this operation because the
* page count is preserved.
*
* Intended to be called by the `LedgerStateFix` transaction on accounts with
* known directory corruption.
*
* @param view The apply view to mutate.
* @param owner The account whose NFToken page directory is to be repaired.
* @return `true` if any correction was applied; `false` if the directory was
* already consistent.
*/
bool
repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner);
/** Ordering predicate for NFToken IDs within and across pages.
*
* Sorts first by the low 96 bits of each ID (the `pageMask` region that
* determines page placement), then by the full 256-bit value as a
* tiebreaker. This ensures deterministic ordering for tokens that share
* the same low 96-bit prefix (equivalent tokens) and must co-reside on
* a single page.
*
* @param a First NFToken ID.
* @param b Second NFToken ID.
* @return `true` if `a` sorts before `b`.
*/
bool
compareTokens(uint256 const& a, uint256 const& b);
/** Modify the URI of an existing NFToken in the owner's directory.
*
* Locates the token's page and updates the `sfURI` field in the token's
* `STObject` within the page's `sfNFTokens` array. If `uri` is
* `std::nullopt`, the `sfURI` field is removed from the token.
*
* @param view The apply view to mutate.
* @param owner The account that owns the token.
* @param nftokenID The 256-bit NFT identifier whose URI is to be changed.
* @param uri The new URI value, or `std::nullopt` to clear the URI.
* @return `tesSUCCESS` on success, or `tecINTERNAL` if the page or token
* cannot be located (indicates ledger inconsistency).
*/
TER
changeTokenURI(
ApplyView& view,
@@ -82,7 +239,33 @@ changeTokenURI(
uint256 const& nftokenID,
std::optional<xrpl::Slice> const& uri);
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
*
* Validates offer parameters that require no ledger access: negative or
* zero amounts (buy offers must carry a non-zero amount), zero IOU amounts,
* zero expiration, and malformed `owner`/`destination` combinations.
* A buy offer must supply `owner` (the targeted token holder); a sell offer
* must not (the seller is implicit). Neither party may designate itself as
* the destination.
*
* Defaults (`owner = nullopt`, `txFlags = tfSellNFToken`) allow
* `NFTokenMint` to reuse this path with minimal adaptation.
*
* @param acctID Account executing the transaction.
* @param amount The offer amount; must be non-negative and, for buy offers,
* non-zero and non-zero for IOUs.
* @param dest Optional destination account that may exclusively accept the
* offer; must not equal `acctID`.
* @param expiration Optional offer expiration; must not be zero.
* @param nftFlags The flags field of the NFToken being offered.
* @param rules Current ledger rule set used for amendment checks.
* @param owner For buy offers, the account that currently holds the token;
* must be absent for sell offers.
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
* buy.
* @return `tesSUCCESS` if all static checks pass, or a `temXXX` error code
* indicating which parameter is invalid.
*/
NotTEC
tokenOfferCreatePreflight(
AccountID const& acctID,
@@ -94,7 +277,37 @@ tokenOfferCreatePreflight(
std::optional<AccountID> const& owner = std::nullopt,
std::uint32_t txFlags = tfSellNFToken);
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint.
*
* Accesses the ledger to validate conditions that cannot be checked
* statically:
* - For non-XRP offers on tokens without `flagCreateTrustLines`, verifies
* that the NFT issuer's trust line for the IOU exists and is not frozen.
* Under `featureNFTokenMintOffer`, an issuer selling their own currency is
* exempt from this check.
* - Enforces `flagTransferable`: if absent and the transacting account is
* neither the issuer nor the current `sfNFTokenMinter`, returns
* `tefNFTOKEN_IS_NOT_TRANSFERABLE`.
* - For buy offers, verifies the account currently has sufficient funds.
* - Verifies `dest` and `owner` accounts exist and have not set
* `lsfDisallowIncomingNFTokenOffer`.
* - Under `fixEnforceNFTokenTrustlineV2`, calls `checkTrustlineAuthorized()`
* to reject offers backed by unauthorized trust lines that carry a balance.
*
* @param view The read-only ledger view.
* @param acctID Account executing the transaction.
* @param nftIssuer Issuer encoded in the NFToken ID.
* @param amount The offer amount.
* @param dest Optional restricted destination account.
* @param nftFlags The flags field of the NFToken being offered.
* @param xferFee Transfer fee encoded in the NFToken ID (basis points).
* @param j Journal for diagnostic logging.
* @param owner For buy offers, the account that currently holds the token.
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
* buy.
* @return `tesSUCCESS` if all ledger-state checks pass, or a `tecXXX` /
* `tefXXX` error code.
*/
TER
tokenOfferCreatePreclaim(
ReadView const& view,
@@ -108,7 +321,28 @@ tokenOfferCreatePreclaim(
std::optional<AccountID> const& owner = std::nullopt,
std::uint32_t txFlags = tfSellNFToken);
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint.
*
* Reserves XRP for the new `ltNFTOKEN_OFFER` object, inserts the offer into
* the account's owner directory and into the token's buy or sell directory
* (determined by `tfSellNFToken` in `txFlags`), constructs the SLE with the
* supplied fields, and increments the owner count.
*
* @param view The apply view to mutate.
* @param acctID Account executing the transaction.
* @param amount The offer amount.
* @param dest Optional restricted destination account.
* @param expiration Optional expiration time for the offer.
* @param seqProxy Sequence or ticket proxy used to derive the offer keylet.
* @param nftokenID The 256-bit ID of the NFToken being offered.
* @param priorBalance The account's XRP balance before the transaction fee
* was deducted; used to verify the reserve requirement.
* @param j Journal for diagnostic logging.
* @param txFlags Transaction flags; `tfSellNFToken` controls offer direction.
* @return `tesSUCCESS` on success, `tecINSUFFICIENT_RESERVE` if the account
* cannot cover the new object reserve, or `tecDIR_FULL` if either
* directory is at capacity.
*/
TER
tokenOfferCreateApply(
ApplyView& view,
@@ -122,6 +356,25 @@ tokenOfferCreateApply(
beast::Journal j,
std::uint32_t txFlags = tfSellNFToken);
/** Verify that an account is authorized to hold a given IOU trust line.
*
* Only active under the `fixEnforceNFTokenTrustlineV2` amendment; returns
* `tesSUCCESS` unconditionally when the amendment is not enabled.
*
* When active, checks that if the IOU issuer requires authorization
* (`lsfRequireAuth`), the trust line between `id` and the issuer exists and
* carries the appropriate `lsfLowAuth` / `lsfHighAuth` flag. The issuer
* account is always considered authorized to hold its own issuance.
*
* @param view The read-only ledger view.
* @param id The account whose authorization is being verified.
* @param j Journal for diagnostic logging.
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
* @return `tesSUCCESS` if authorized, `tecNO_ISSUER` if the issuer account
* does not exist, `tecNO_LINE` if the required trust line is absent, or
* `tecNO_AUTH` if the trust line exists but is not authorized.
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
*/
TER
checkTrustlineAuthorized(
ReadView const& view,
@@ -129,6 +382,26 @@ checkTrustlineAuthorized(
beast::Journal const j,
Issue const& issue);
/** Verify that an IOU trust line is not deep-frozen for a given account.
*
* Only active under the `featureDeepFreeze` amendment; returns
* `tesSUCCESS` unconditionally when the amendment is not enabled.
*
* When active, checks whether the trust line between `id` and the IOU issuer
* carries either `lsfLowDeepFreeze` or `lsfHighDeepFreeze`. Either side
* enacting deep freeze blocks token receipt, regardless of which party set it.
* The issuer account is always permitted to accept its own issuance; accounts
* with no trust line are treated as not frozen.
*
* @param view The read-only ledger view.
* @param id The account whose deep-freeze status is being checked.
* @param j Journal for diagnostic logging.
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
* @return `tesSUCCESS` if not deep-frozen or if no trust line exists,
* `tecNO_ISSUER` if the issuer account does not exist, or `tecFROZEN`
* if the trust line is deep-frozen.
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
*/
TER
checkTrustlineDeepFrozen(
ReadView const& view,

View File

@@ -9,18 +9,38 @@
namespace xrpl {
/** Delete an offer.
Requirements:
The offer must exist.
The caller must have already checked permissions.
@param view The ApplyView to modify.
@param sle The offer to delete.
@param j Journal for logging.
@return tesSUCCESS on success, otherwise an error code.
*/
/** Remove an offer and its directory back-references from the ledger.
*
* Performs the full teardown sequence atomically within the transaction
* buffer: removes the offer from the owner's directory, removes it from
* the order-book quality directory, decrements the owner's reserve count,
* and erases the SLE. For hybrid offers (flagged `lsfHybrid`) that
* participate in one or more Permissioned DEX domains, each entry in
* `sfAdditionalBooks` is also removed from its domain-specific book
* directory before the owner-count adjustment and erasure.
*
* If `sle` is null the function returns `tesSUCCESS` immediately,
* allowing callers to pass the result of a failed `peek()` without
* a pre-check (defensive against double-delete within one batch).
*
* @pre The offer SLE must exist in the ledger and both its
* `sfOwnerNode` and `sfBookNode` back-references must be valid.
* @pre The caller must have already verified that the submitting
* account is authorized to delete this offer; this function
* performs no ownership or permission check.
*
* @param view The `ApplyView` transaction buffer to modify.
* @param sle The offer SLE to delete. May be null (treated as no-op).
* @param j Journal for diagnostic logging.
*
* @return `tesSUCCESS` on success, or `tefBAD_LEDGER` if a directory
* back-reference is missing (invariant violation; should not occur
* in a well-formed ledger).
*
* @note `[[nodiscard]]` is intentionally absent: `BookTip` and payment
* path callers do not always inspect the return value, and enforcing
* the attribute would have broken compilation across the engine.
*/
// [[nodiscard]] // nodiscard commented out so Flow, BookTip and others compile.
TER
offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j);

View File

@@ -7,6 +7,35 @@
namespace xrpl {
/** Tear down a payment channel and return unspent XRP to its source account.
*
* Performs four ledger mutations in order:
* 1. Removes the channel from the source's owner directory (`sfOwnerNode`).
* 2. Conditionally removes the channel from the destination's owner directory
* (`sfDestinationNode`) — the field is absent on older channel objects that
* predate destination-directory tracking, so its presence is tested before
* the removal attempt.
* 3. Credits the unspent balance (`sfAmount - sfBalance`) back to the source
* account. `sfAmount` is the total XRP escrowed; `sfBalance` is the
* cumulative amount already paid to the destination.
* 4. Decrements the source's owner count and erases the `ltPAYCHAN` SLE.
*
* Called by both `PaymentChannelClaim` and `PaymentChannelFund` whenever a
* channel must be closed — on expiry (`cancelAfter`/`expiration` elapsed), on
* an explicit `tfClose` flag, or when the channel is fully drained.
*
* @param slep The `ltPAYCHAN` SLE to close; must satisfy
* `sfAmount >= sfBalance` (asserted).
* @param view The apply view through which all ledger mutations are made.
* @param key The ledger key of the channel SLE (used for directory removal).
* @param j Journal for fatal-level diagnostic messages on internal errors.
* @return `tesSUCCESS` on the normal path; `tefBAD_LEDGER` if an owner
* directory removal fails (indicates corrupted ledger state);
* `tefINTERNAL` if the source account SLE cannot be found.
* @note The `tefBAD_LEDGER` and `tefINTERNAL` branches are annotated
* `LCOV_EXCL` — they guard against ledger corruption that cannot occur
* during correct operation.
*/
TER
closeChannel(
std::shared_ptr<SLE> const& slep,

View File

@@ -1,13 +1,90 @@
/**
* @file PermissionedDEXHelpers.h
* @brief Domain membership predicates for the Permissioned DEX.
*
* Declares the two authorization gatekeepers used by `xrpl::permissioned_dex`
* to enforce credential-based access control on restricted order books.
* Both functions are called from transaction preclaim logic and from live
* order-book traversal in `OfferStream`.
*/
#pragma once
#include <xrpl/ledger/View.h>
namespace xrpl::permissioned_dex {
// Check if an account is in a permissioned domain
/**
* @brief Test whether an account currently qualifies as a member of a
* permissioned domain.
*
* Resolves the `PermissionedDomain` ledger object identified by @p domainID
* and applies a two-tier membership test:
*
* 1. **Owner shortcut** — the domain's `sfOwner` is always considered a member,
* avoiding a bootstrap problem where the owner couldn't trade in their own
* domain.
* 2. **Credential scan** — for all other accounts, the function iterates
* `sfAcceptedCredentials` and returns `true` as soon as it finds a
* credential issued to @p account that (a) carries the `lsfAccepted` flag
* and (b) has not expired according to `credentials::checkExpired` evaluated
* against the ledger's `parentCloseTime`.
*
* Expiry is evaluated against `parentCloseTime` (not wall time) so that all
* validators reach the same deterministic result regardless of local clock skew.
*
* @param view The read-only ledger view to query.
* @param account The account whose domain membership is being tested.
* @param domainID The identifier of the `PermissionedDomain` ledger object.
* @return `true` if @p account is the domain owner or holds at least one
* accepted, non-expired credential listed in the domain; `false` if the
* domain object does not exist, or if no qualifying credential is found.
*
* @note Called from `OfferCreate` preclaim (rejects with `tecNO_PERMISSION` if
* `false`) and twice from `Payment` preclaim — once for the sender, once for
* the destination — since a domain payment requires both parties to be
* members. Also called internally by `offerInDomain`.
*/
[[nodiscard]] bool
accountInDomain(ReadView const& view, AccountID const& account, Domain const& domainID);
// Check if an offer is in the permissioned domain
/**
* @brief Test whether a specific offer is still legitimately part of a
* permissioned domain at the time it is being consumed.
*
* Called by `OfferStream` during order-book traversal to handle the race
* between offer creation and subsequent credential expiry. An offer that was
* valid when placed may become invalid if the owner's credentials expire before
* the offer is matched. When this function returns `false`, `OfferStream`
* removes the offer from the book immediately (`permRmOffer`) instead of
* matching it.
*
* The function performs the following checks in order:
* - Offer SLE must exist (defensive; should not occur in a well-formed book).
* - Offer must carry `sfDomainID` (defensive; should not occur).
* - `sfDomainID` must match @p domainID (defensive; should not occur).
* - **Post-`fixSecurity3_1_3`**: a hybrid offer (`lsfHybrid`) must have
* `sfAdditionalBooks` present with exactly one entry; a violation is logged
* as an error and `false` is returned.
* - **Pre-`fixSecurity3_1_3`**: a hybrid offer must have `sfAdditionalBooks`
* present (size is not validated).
* - Delegates the final membership check to `accountInDomain` for the offer's
* owner (`sfAccount`).
*
* The three defensive checks are marked `LCOV_EXCL_LINE`; they guard against
* invariant violations that cannot occur under normal operation but are retained
* as safety nets.
*
* @param view The read-only ledger view to query.
* @param offerID The hash identifier of the offer SLE to validate.
* @param domainID The permissioned domain the offer is expected to belong to.
* @param j Journal used to log an error if a hybrid offer has a missing
* or malformed `sfAdditionalBooks` field.
* @return `true` if the offer passes all structural checks and its owner is
* currently a member of @p domainID; `false` otherwise.
*
* @note The `fixSecurity3_1_3` amendment tightens hybrid-offer validation from
* a presence-only check on `sfAdditionalBooks` to a presence-plus-size-one
* check. Both code paths must be preserved for deterministic historic replay.
*/
[[nodiscard]] bool
offerInDomain(
ReadView const& view,

View File

@@ -1,3 +1,23 @@
/** @file
* IOU trustline (RippleState) operations for the XRP Ledger.
*
* Declares every ledger operation that reads from or writes to a
* `RippleState` (trustline) SLE: credit-limit and balance queries,
* freeze checks, trustline lifecycle, IOU issuance/redemption,
* authorization and rippling enforcement, zero-balance holding
* management, and AMM-specific cleanup.
*
* This file is the IOU-specific leaf of the token helper layer.
* Asset-agnostic callers should go through the dispatchers in
* `TokenHelpers.h`, which branch on `Issue` vs `MPTIssue` and
* delegate here for the IOU path.
*
* @note The trustline orientation invariant is pervasive here:
* `sfLowLimit` always belongs to the account whose `AccountID`
* compares less; `sfHighLimit` to the other. Every function
* applies this flip internally — callers supply `(account, issuer)`
* and receive results in account-centric terms.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -10,27 +30,29 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
//------------------------------------------------------------------------------
//
// RippleState (Trustline) helpers
//
//------------------------------------------------------------------------------
// --- RippleState (Trustline) helpers ---
namespace xrpl {
//------------------------------------------------------------------------------
//
// Credit functions (from Credit.h)
//
//------------------------------------------------------------------------------
// --- Credit queries ---
/** Calculate the maximum amount of IOUs that an account can hold
@param view the ledger to check against.
@param account the account of interest.
@param issuer the issuer of the IOU.
@param currency the IOU to check.
@return The maximum amount that can be held.
*/
/** Read the maximum IOU balance that @p account has authorised @p issuer to
* carry on their behalf.
*
* Reads `sfLowLimit` or `sfHighLimit` from the trustline depending on
* which side `account` occupies (low if `account < issuer`). The issuer
* field of the returned amount is rewritten to `account` so the result is
* safe to consume without knowing the binary-ordering of the two accounts.
* Returns a zero-valued `STAmount` (with the correct issue) if no trustline
* exists.
*
* @param view Read-only ledger view to query.
* @param account The account whose credit limit is requested.
* @param issuer The IOU issuer.
* @param currency The currency of the trustline.
* @return The credit limit expressed from @p account's perspective, or zero
* if no trustline exists.
*/
/** @{ */
STAmount
creditLimit(
@@ -39,16 +61,35 @@ creditLimit(
AccountID const& issuer,
Currency const& currency);
/** Convenience wrapper returning the credit limit as `IOUAmount`.
*
* @param v Read-only ledger view to query.
* @param acc The account whose credit limit is requested.
* @param iss The IOU issuer.
* @param cur The currency of the trustline.
* @return The credit limit as `IOUAmount`, or zero if no trustline exists.
* @see creditLimit
*/
IOUAmount
creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur);
/** @} */
/** Returns the amount of IOUs issued by issuer that are held by an account
@param view the ledger to check against.
@param account the account of interest.
@param issuer the issuer of the IOU.
@param currency the IOU to check.
*/
/** Read the IOU balance that @p account currently holds.
*
* `sfBalance` is stored in "low-account-sends-to-high-account" orientation.
* When `account` is the high side the stored value is negated before being
* returned, so callers always receive a balance expressed as "how much of
* this currency does @p account hold", regardless of which slot they occupy
* on the trustline. Returns zero (with the correct issue) if no trustline
* exists.
*
* @param view Read-only ledger view to query.
* @param account The account whose balance is requested.
* @param issuer The IOU issuer.
* @param currency The currency of the trustline.
* @return The balance expressed from @p account's perspective, or zero if
* no trustline exists.
*/
/** @{ */
STAmount
creditBalance(
@@ -58,12 +99,20 @@ creditBalance(
Currency const& currency);
/** @} */
//------------------------------------------------------------------------------
//
// Freeze checking (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Freeze checks (IOU-specific) ---
/** Check whether @p issuer has individually frozen @p account's trustline.
*
* Inspects only the issuer's side flag (`lsfLowFreeze`/`lsfHighFreeze`) on
* the trustline. Does **not** check the issuer's global freeze flag — use
* `isFrozen` for that combined check. Always returns `false` for XRP.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the issuer has set a line-level freeze on this account.
*/
[[nodiscard]] bool
isIndividualFrozen(
ReadView const& view,
@@ -71,12 +120,34 @@ isIndividualFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the issuer has set a line-level freeze on this account.
* @see isIndividualFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isIndividualFrozen(view, account, issue.currency, issue.account);
}
/** Check whether @p account is frozen for @p currency issued by @p issuer.
*
* Returns `true` if either the issuer's `AccountRoot` has `lsfGlobalFreeze`
* set, or the issuer has frozen this specific trustline (`lsfLowFreeze` /
* `lsfHighFreeze`). Always returns `false` for XRP or when
* `account == issuer`. This is the check used by payment paths.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the account cannot move this IOU due to any freeze.
*/
[[nodiscard]] bool
isFrozen(
ReadView const& view,
@@ -84,20 +155,52 @@ isFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the account cannot move this IOU due to any freeze.
* @see isFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isFrozen(view, account, issue.currency, issue.account);
}
// Overload with depth parameter for uniformity with MPTIssue version.
// The depth parameter is ignored for IOUs since they don't have vault recursion.
/** Overload accepting a depth parameter for interface uniformity with MPT.
*
* IOUs do not have vault-level recursion, so the `depth` argument is
* unconditionally ignored.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the account cannot move this IOU due to any freeze.
*/
[[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue, int /*depth*/)
{
return isFrozen(view, account, issue);
}
/** Check whether @p account is deep-frozen for @p currency issued by
* @p issuer.
*
* Deep-freeze (`lsfHighDeepFreeze` / `lsfLowDeepFreeze`) is a stricter
* condition than ordinary freeze: it prevents both sending *and* receiving
* the currency. Always returns `false` for XRP, and always returns `false`
* when `issuer == account` (an issuer cannot deep-freeze their own balance
* with themselves).
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the deep-freeze flag is set on either side of the line.
*/
[[nodiscard]] bool
isDeepFrozen(
ReadView const& view,
@@ -105,6 +208,18 @@ isDeepFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`, with an optional depth parameter
* for interface uniformity with the MPT equivalent.
*
* The `depth` argument is unconditionally ignored for IOUs.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the deep-freeze flag is set on either side of the line.
* @see isDeepFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isDeepFrozen(
ReadView const& view,
@@ -115,22 +230,63 @@ isDeepFrozen(
return isDeepFrozen(view, account, issue.currency, issue.account);
}
/** Convert a deep-freeze check into a `TER` result.
*
* Convenience wrapper for transactor preflight code that returns
* `tecFROZEN` if the account is deep-frozen and `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `tecFROZEN` if deep-frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] inline TER
checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Trust line operations
//
//------------------------------------------------------------------------------
// --- Trust line lifecycle ---
/** Create a trust line
This can set an initial balance.
*/
/** Create a new `RippleState` (trustline) SLE and insert it into both owner
* directories.
*
* This is the lowest-level entry point for trustline creation. It is called
* directly by `TrustSet` transactors and indirectly by `issueIOU` when the
* destination has no existing line.
*
* The function writes all trustline fields — limits, quality in/out, balance,
* and flag bits — using side-aware field selectors (`sfLowLimit`/`sfHighLimit`
* etc.) derived from `bSrcHigh`. The peer account's `lsfNoRipple` flag is
* initialised from the peer's `lsfDefaultRipple` setting (absent means
* noRipple is on by default).
*
* @param view Mutable ledger view.
* @param bSrcHigh `true` if `uSrcAccountID` occupies the "high" slot
* (i.e., `uSrcAccountID > uDstAccountID`).
* @param uSrcAccountID The account whose limit and flags are being
* configured.
* @param uDstAccountID The peer account on the other side of the line.
* @param uIndex Pre-calculated keylet key for the new SLE.
* @param sleAccount The `AccountRoot` SLE for the account being set
* (used to adjust owner count); must not be null.
* @param bAuth If `true`, set the authorization flag on the source
* side of the line.
* @param bNoRipple If `true`, set `lsfNoRipple` on the source side.
* @param bFreeze If `true`, set the freeze flag on the source side.
* @param bDeepFreeze If `true`, set the deep-freeze flag on the source
* side.
* @param saBalance Initial balance from the source account's
* perspective; the issuer field must be `noAccount()`.
* @param saLimit Credit limit for the source account; the issuer
* field must be `uSrcAccountID`.
* @param uQualityIn Quality-in override (0 = default/no override).
* @param uQualityOut Quality-out override (0 = default/no override).
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tecDIR_FULL` if either owner directory
* is at capacity, `tecNO_TARGET` if the peer account does not exist,
* or `tefINTERNAL` if `sleAccount` is null or has a mismatched ID.
*/
[[nodiscard]] TER
trustCreate(
ApplyView& view,
@@ -151,6 +307,21 @@ trustCreate(
std::uint32_t uQualityOut,
beast::Journal j);
/** Delete a `RippleState` (trustline) SLE and remove its directory backlinks.
*
* Removes the SLE from both the low and high owner directories using the
* `sfLowNode`/`sfHighNode` deletion hints stored inside the SLE itself,
* then erases the SLE from the view.
*
* @param view Mutable ledger view.
* @param sleRippleState The trustline SLE to delete; must be obtained
* from `view.peek()`.
* @param uLowAccountID The account occupying the low slot.
* @param uHighAccountID The account occupying the high slot.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if either directory
* removal fails (indicating ledger corruption).
*/
[[nodiscard]] TER
trustDelete(
ApplyView& view,
@@ -159,12 +330,30 @@ trustDelete(
AccountID const& uHighAccountID,
beast::Journal j);
//------------------------------------------------------------------------------
//
// IOU issuance/redemption
//
//------------------------------------------------------------------------------
// --- IOU issuance/redemption ---
/** Issue IOUs from @p issue.account to @p account, adjusting the trustline
* balance.
*
* Debits the issuer's side of the trustline and credits the receiver. After
* adjusting the balance, calls the internal `updateTrustLine` helper: if the
* sender's balance crosses zero and seven specific cleanup conditions are met
* (zero limit, no freeze, etc.), the sender's reserve is released and the
* line may be deleted via `trustDelete`.
*
* If no trustline exists for the receiver, one is created via `trustCreate`,
* inheriting the receiver's `lsfDefaultRipple` setting for the initial
* `lsfNoRipple` state. Always invokes `view.creditHookIOU()` after mutating
* the balance.
*
* @param view Mutable ledger view.
* @param account The account receiving the IOUs (must not be the issuer).
* @param amount The amount to issue; its `Issue` must match @p issue.
* @param issue Identifies the currency and issuer.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tef`/`tec` code propagated from
* `trustCreate` or `trustDelete` if an error occurs.
*/
[[nodiscard]] TER
issueIOU(
ApplyView& view,
@@ -173,6 +362,26 @@ issueIOU(
Issue const& issue,
beast::Journal j);
/** Redeem IOUs held by @p account back toward the issuer, adjusting the
* trustline balance.
*
* The mirror image of `issueIOU`: credits the issuer and debits the holder.
* After adjusting the balance, calls `updateTrustLine` for the same
* automatic cleanup logic. Always invokes `view.creditHookIOU()` after
* mutating the balance.
*
* Unlike `issueIOU`, a missing trustline is treated as a fatal internal
* error (`tefINTERNAL`) because it is impossible to redeem a balance on a
* line that does not exist.
*
* @param view Mutable ledger view.
* @param account The account redeeming IOUs (must not be the issuer).
* @param amount The amount to redeem; its `Issue` must match @p issue.
* @param issue Identifies the currency and issuer.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefINTERNAL` if no trustline exists,
* or a `tef`/`tec` code from `trustDelete` if cleanup triggers an error.
*/
[[nodiscard]] TER
redeemIOU(
ApplyView& view,
@@ -181,28 +390,30 @@ redeemIOU(
Issue const& issue,
beast::Journal j);
//------------------------------------------------------------------------------
//
// Authorization and transfer checks (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Authorization and transfer checks (IOU-specific) ---
/** Check if the account lacks required authorization.
/** Check whether @p account is authorized to hold the IOU described by
* @p issue.
*
* Return tecNO_AUTH or tecNO_LINE if it does
* and tesSUCCESS otherwise.
* Behaviour depends on @p authType:
* - **`StrongAuth`**: Returns `tecNO_LINE` immediately if no trustline
* exists. If the issuer has `lsfRequireAuth` and the line exists but is
* not authorized, returns `tecNO_AUTH`.
* - **`WeakAuth`** / **`Legacy`** (equivalent for IOUs): Returns
* `tecNO_AUTH` if `lsfRequireAuth` is set, the line exists, but is not
* authorized. Returns `tecNO_LINE` if auth is required and no line
* exists. If `lsfRequireAuth` is not set, returns `tesSUCCESS` even when
* no line exists — appropriate for payment path-finding where a line may
* be created on the fly.
*
* If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return
* tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the
* RippleState does exist, and the RippleState is not authorized.
* Always returns `tesSUCCESS` for XRP or when `account == issue.account`.
*
* If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the
* RippleState exists, and is not authorized. Return tecNO_LINE if
* lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if
* WeakAuth and lsfRequireAuth is *not* set, this function will return
* tesSUCCESS even if RippleState does *not* exist.
*
* The default "Legacy" auth type is equivalent to WeakAuth.
* @param view Read-only ledger view.
* @param issue The IOU to check authorization for.
* @param account The account to check.
* @param authType Authorization strictness; defaults to `AuthType::Legacy`
* (equivalent to `WeakAuth` for IOUs).
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE`.
*/
[[nodiscard]] TER
requireAuth(
@@ -211,21 +422,53 @@ requireAuth(
AccountID const& account,
AuthType authType = AuthType::Legacy);
/** Check if the destination account is allowed
* to receive IOU. Return terNO_RIPPLE if rippling is
* disabled on both sides and tesSUCCESS otherwise.
/** Check whether an IOU can be transferred between @p from and @p to via the
* issuer's trustlines.
*
* Returns `tesSUCCESS` unconditionally when either endpoint is the issuer,
* or when the IOU is native (XRP). For third-party transfers, returns
* `terNO_RIPPLE` only when both the `from` and the `to` trustlines have
* `lsfNoRipple` set on the issuer's side, blocking rippling through. If a
* trustline does not exist for a given account, the issuer's
* `lsfDefaultRipple` flag is consulted as a fallback preference.
*
* @param view Read-only ledger view.
* @param issue The IOU (identifies the issuer and currency).
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, `terNO_RIPPLE` if
* rippling is disabled on both sides.
*/
[[nodiscard]] TER
canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to);
//------------------------------------------------------------------------------
//
// Empty holding operations (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Empty holding operations (IOU-specific) ---
/// Any transactors that call addEmptyHolding() in doApply must call
/// canAddHolding() in preflight with the same View and Asset
/** Create a zero-balance trustline for @p accountID, reserving the destination
* slot before any funds arrive.
*
* Used by transactors (e.g., DEX limit orders) that need to guarantee a
* destination line exists before settlement. Checks that @p accountID can
* cover the increased owner-count reserve before calling `trustCreate`.
*
* Returns `tesSUCCESS` immediately for XRP or when `accountID` is the
* issuer. Returns `tecDUPLICATE` if the trustline already exists.
*
* @note Any transactor that calls this function in `doApply` **must** call
* `canAddHolding()` (declared in `TokenHelpers.h`) in `preflight` with
* the same view and asset to validate the reserve precondition.
*
* @param view Mutable ledger view.
* @param accountID The account that will hold the IOU.
* @param priorBalance The account's XRP balance before the current
* transaction, used to test reserve sufficiency.
* @param issue The IOU to create a holding for.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecFROZEN` if the issuer is globally
* frozen; `tecNO_LINE_INSUF_RESERVE` if the account cannot afford the
* reserve; `tecDUPLICATE` if the line already exists; or a `tec`/`tef`
* code from `trustCreate`.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -234,6 +477,20 @@ addEmptyHolding(
Issue const& issue,
beast::Journal journal);
/** Delete a zero-balance trustline previously created by `addEmptyHolding`.
*
* Validates that the balance is actually zero before deletion. Adjusts
* owner counts for both the low and high sides if their reserve flags are
* set, then calls `trustDelete`.
*
* @param view Mutable ledger view.
* @param accountID The account whose holding line should be removed.
* @param issue The IOU identifying the trustline to remove.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
* non-zero; `tecOBJECT_NOT_FOUND` if no line exists (and the account
* is not the issuer); or a `tef`/`tec` code from `trustDelete`.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -241,9 +498,27 @@ removeEmptyHolding(
Issue const& issue,
beast::Journal journal);
/** Delete trustline to AMM. The passed `sle` must be obtained from a prior
* call to view.peek(). Fail if neither side of the trustline is AMM or
* if ammAccountID is seated and is not one of the trustline's side.
/** Delete a trustline owned by an AMM pool account during AMM withdrawal.
*
* Validates that:
* - @p sleState is a non-null `ltRIPPLE_STATE` SLE.
* - Exactly one of the two trustline endpoints is an AMM account
* (identified by the presence of `sfAMMID` in the `AccountRoot`).
* - If @p ammAccountID is provided, it matches one of the endpoints.
*
* On success, calls `trustDelete` and decrements the owner count of the
* non-AMM side.
*
* @param view Mutable ledger view.
* @param sleState The `ltRIPPLE_STATE` SLE to delete; must be obtained
* from `view.peek()`.
* @param ammAccountID If provided, the expected AMM account ID; the
* function returns `terNO_AMM` if neither endpoint matches.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecINTERNAL` if the SLE is null, has
* the wrong type, if both sides are AMM, or if the reserve flag is
* unexpectedly absent; `terNO_AMM` if neither endpoint is an AMM or
* the optional ID does not match; or a `tef` code from `trustDelete`.
*/
[[nodiscard]] TER
deleteAMMTrustLine(
@@ -252,8 +527,19 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior
* call to view.peek().
/** Delete an AMM account's `MPToken` SLE during AMM withdrawal.
*
* Removes the `MPToken` SLE from @p ammAccountID's owner directory and
* erases it from the view. The caller is responsible for any balance
* assertions before invoking this function.
*
* @param view Mutable ledger view.
* @param sleMPT The `MPToken` SLE to delete; must be obtained from
* `view.peek()`.
* @param ammAccountID The AMM account that owns the `MPToken`.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if the directory removal
* fails (indicating ledger corruption).
*/
[[nodiscard]] TER
deleteAMMMPToken(

View File

@@ -1,3 +1,20 @@
/** @file
* Asset-agnostic dispatcher layer for all token operations on the XRP Ledger.
*
* This header is the unified entry point for token operations that must work
* across XRPL's three asset classes: XRP, IOU (trust-line-based), and MPT
* (Multi-Party Token). It sits between transaction-processing code that wants
* to be asset-agnostic and the two type-specific leaf modules:
* `RippleStateHelpers.h` for IOU trust lines and `MPTokenHelpers.h` for
* `MPToken`/`MPTokenIssuance` objects.
*
* Callers pass an `Asset` — a `std::variant<Issue, MPTIssue>` — and the
* functions here dispatch via `std::visit` or `Asset::visit` to the correct
* lower-level function, returning consistent result types (`STAmount`, `TER`,
* `bool`) regardless of asset kind. Adding a new asset type requires only
* extending the `Asset` variant and the branches here, not modifying call
* sites.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -20,30 +37,83 @@ namespace xrpl {
//
//------------------------------------------------------------------------------
/** Controls the treatment of frozen account balances */
enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen };
/** Controls the treatment of unauthorized MPT balances */
enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized };
/** Controls whether to include the account's full spendable balance */
enum class SpendableHandling { SimpleBalance, FullBalance };
enum class WaiveTransferFee : bool { No = false, Yes };
/** Controls whether accountSend is allowed to overflow OutstandingAmount **/
enum class AllowMPTOverflow : bool { No = false, Yes };
/* Check if MPToken (for MPT) or trust line (for IOU) exists:
* - StrongAuth - before checking if authorization is required
* - WeakAuth
* for MPT - after checking lsfMPTRequireAuth flag
* for IOU - do not check if trust line exists
* - Legacy
* for MPT - before checking lsfMPTRequireAuth flag i.e. same as StrongAuth
* for IOU - do not check if trust line exists i.e. same as WeakAuth
/** Controls how a frozen balance is reported by balance-query functions.
*
* Use `ZeroIfFrozen` in payment paths where a frozen balance must not be
* spent. Use `IgnoreFreeze` in cleanup paths that need the real value
* regardless of freeze state.
*/
enum class AuthType { StrongAuth, WeakAuth, Legacy };
enum class FreezeHandling {
IgnoreFreeze, /**< Return the actual balance even if the holding is frozen. */
ZeroIfFrozen /**< Return zero when the holding is frozen (the spendable amount). */
};
/** Controls how an unauthorized MPT balance is reported by balance-query functions.
*
* Parallel to `FreezeHandling` but for MPT authorization. Use
* `ZeroIfUnauthorized` when computing the amount an account may legally spend.
*/
enum class AuthHandling {
IgnoreAuth, /**< Return the actual balance even if the MPToken is unauthorized. */
ZeroIfUnauthorized /**< Return zero when the MPToken is not authorized. */
};
/** Controls whether `accountHolds` reports simple or full spendable balance.
*
* - `SimpleBalance`: the amount the account can spend without going into
* debt, i.e. the raw trustline balance (negated to account-centric terms)
* for IOU, or the `sfMPTAmount` for MPT.
* - `FullBalance`: for IOU, also includes the peer's credit limit so the
* account can borrow up to that limit; for the IOU issuer, returns
* `STAmount::kMAX_VALUE`; for the MPT issuer, returns
* `MaximumAmount - OutstandingAmount`.
*/
enum class SpendableHandling {
SimpleBalance, /**< Balance the account can spend without going into debt. */
FullBalance /**< Full spendable balance including borrowable credit or issuance capacity. */
};
/** Controls whether the transfer fee is skipped during a send operation.
*
* Typed as `enum class : bool` to prevent accidental transposition with
* other boolean parameters at call sites.
*/
enum class WaiveTransferFee : bool {
No = false, /**< Apply the normal transfer fee. */
Yes /**< Skip the transfer fee entirely. */
};
/** Controls whether `accountSend` permits `OutstandingAmount` to transiently
* exceed `MaximumAmount` during MPT payment-engine routing.
*
* The payment engine issues tokens first (raising `OutstandingAmount`) and
* redeems them in the same transaction (lowering it back). `Yes` raises the
* overflow ceiling to `UINT64_MAX` for that transient window. Direct sends
* use `No` and enforce the strict `MaximumAmount` cap.
*/
enum class AllowMPTOverflow : bool {
No = false, /**< Enforce the strict MaximumAmount cap. */
Yes /**< Allow transient overflow up to UINT64_MAX during routing. */
};
/** Encodes the three-way authorization-strictness contract.
*
* Determines how `requireAuth` behaves when checking whether an account may
* hold or interact with a token:
* - `StrongAuth` checks that the holding object (trust line or `MPToken`)
* exists *before* asking whether authorization is set. Returns `tecNO_LINE`
* immediately if no holding exists.
* - `WeakAuth` skips the existence check, returning `tesSUCCESS` when
* authorization is not required even if no holding exists. Appropriate for
* payment path-finding where a line may be created on the fly.
* - `Legacy` maps to `StrongAuth` for MPT and `WeakAuth` for IOU, preserving
* historical behavior at existing call sites.
*/
enum class AuthType {
StrongAuth, /**< Existence of the holding object is verified first. */
WeakAuth, /**< Holding existence is not required when auth is not needed. */
Legacy /**< StrongAuth for MPT; WeakAuth for IOU (historical default). */
};
//------------------------------------------------------------------------------
//
@@ -51,35 +121,126 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy };
//
//------------------------------------------------------------------------------
/** Check whether the issuer of @p asset has activated a global freeze.
*
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
* A global freeze on the issuer's `AccountRoot` blocks all holders
* simultaneously.
*
* @param view Read-only ledger view.
* @param asset The asset to test.
* @return `true` if the issuer has a global freeze in effect.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, Asset const& asset);
/** Check whether @p account has an individual freeze on @p asset.
*
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
* For IOU, checks the issuer's per-line freeze flag. For MPT, checks the
* `lsfMPTLocked` flag on the `MPToken` SLE. Does not check global freeze.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `true` if the issuer has set an individual freeze on this account.
*/
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
/** Check whether @p account is frozen for @p asset (global or individual).
*
* Returns `true` if either `isGlobalFrozen` or `isIndividualFrozen` is true
* for the given account and asset. Dispatches to the typed IOU or MPT leaf
* via `std::visit`.
*
* The `depth` parameter enables recursive vault checking: if @p asset is an
* MPT backed by a vault, the vault's underlying asset is checked up to
* `maxAssetCheckDepth` levels deep.
*
* @note Recursion is purely defensive. The ledger currently does not allow
* nested vaults to be created, so `depth > 0` should not occur in
* practice.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @param depth Current recursion depth for vault checking; defaults to 0.
* @return `true` if the account cannot move this asset due to any freeze.
*/
[[nodiscard]] bool
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
/** Convert a freeze check on an IOU to a `TER`.
*
* Returns `tecFROZEN` if `isFrozen` is true for the given account and issue,
* `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU to test.
* @return `tecFROZEN` if frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue);
/** Convert a freeze check on an MPT to a `TER`.
*
* Returns `tecLOCKED` (not `tecFROZEN`) if `isFrozen` is true for the given
* account and MPT issuance, `tesSUCCESS` otherwise. The distinct error code
* reflects the separate protocol semantics of MPT locking vs IOU freezing.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @return `tecLOCKED` if frozen/locked, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Convert a freeze check on any asset to a `TER`.
*
* Dispatches to `checkFrozen(…, Issue)` or `checkFrozen(…, MPTIssue)` based
* on the runtime type of @p asset, returning the type-appropriate error code
* (`tecFROZEN` for IOU, `tecLOCKED` for MPT).
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if frozen, `tesSUCCESS`
* otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
/** Check whether any account in @p accounts is frozen for @p issue.
*
* Iterates the list and returns `true` on the first frozen account. Used to
* check both sides (taker and maker) of an offer with a single call.
*
* @param view Read-only ledger view.
* @param accounts The accounts to test, e.g. `{takerID, makerID}`.
* @param issue The IOU to test.
* @return `true` if any account in the list is frozen for @p issue.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
std::initializer_list<AccountID> const& accounts,
Issue const& issue);
/** Check whether any account in @p accounts is frozen for @p asset.
*
* Asset-dispatching overload. Delegates to the IOU or MPT leaf for each
* account in the list. The `depth` parameter passes through to `isFrozen`
* for vault-backed MPT recursion.
*
* @param view Read-only ledger view.
* @param accounts The accounts to test.
* @param asset The asset to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if any account in the list is frozen for @p asset.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
@@ -87,6 +248,22 @@ isAnyFrozen(
Asset const& asset,
int depth = 0);
/** Check whether @p account is deep-frozen for @p mptIssue.
*
* For MPT, deep-freeze semantics are identical to regular freeze: a frozen
* MPT holder cannot send or receive. This function delegates to
* `isFrozen(view, account, mptIssue, depth)`.
*
* @note For IOU, deep-freeze is a distinct state (`lsfDeepFreeze`) where the
* holder cannot send but can still receive. See `isDeepFrozen` in
* `RippleStateHelpers.h` for IOU-specific semantics.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if the account is frozen/locked for this MPT.
*/
[[nodiscard]] bool
isDeepFrozen(
ReadView const& view,
@@ -94,17 +271,51 @@ isDeepFrozen(
MPTIssue const& mptIssue,
int depth = 0);
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
/** Check whether @p account is deep-frozen for @p asset.
*
* Dispatches to the IOU or MPT leaf via `std::visit`. For MPT, deep-freeze
* is equivalent to regular freeze. For IOU, checks the `lsfDeepFreeze` flag,
* which prevents sending but allows receiving.
*
* The `depth` parameter enables recursive vault checking up to
* `maxAssetCheckDepth` levels.
*
* @note Recursion is purely defensive — nested vaults cannot currently be
* created on the ledger.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if the account is deep-frozen for @p asset.
*/
[[nodiscard]] bool
isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
/** Convert a deep-freeze check on an MPT to a `TER`.
*
* Returns `tecLOCKED` if `isDeepFrozen` is true, `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @return `tecLOCKED` if deep-frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Convert a deep-freeze check on any asset to a `TER`.
*
* Dispatches to `checkDeepFrozen(…, Issue)` (`tecFROZEN`) or
* `checkDeepFrozen(…, MPTIssue)` (`tecLOCKED`) based on the runtime type of
* @p asset.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if deep-frozen,
* `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
@@ -114,19 +325,31 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
//
//------------------------------------------------------------------------------
// Returns the amount an account can spend.
//
// If shSIMPLE_BALANCE is specified, this is the amount the account can spend
// without going into debt.
//
// If shFULL_BALANCE is specified, this is the amount the account can spend
// total. Specifically:
// * The account can go into debt if using a trust line, and the other side has
// a non-zero limit.
// * If the account is the asset issuer the limit is defined by the asset /
// issuance.
//
// <-- saAmount: amount of currency held by account. May be negative.
/** Return the amount that @p account can spend of the given currency/issuer.
*
* This is the canonical implementation. All other `accountHolds` overloads
* ultimately delegate here for the IOU path.
*
* - For XRP: returns `xrpLiquid(view, account, 0, j)` (reserve-adjusted).
* - For IOU with `shFULL_BALANCE` when `account == issuer`: returns
* `STAmount::kMAX_VALUE` — the issuer has effectively unlimited issuance
* capacity.
* - For IOU otherwise: reads the trust-line balance from the ledger,
* negating it to account-centric terms. If `shFULL_BALANCE` is specified,
* also adds the peer's credit limit so the account can draw down that
* credit. Returns zero if the line is frozen (when `ZeroIfFrozen`) or does
* not exist.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @param zeroIfFrozen Whether to return zero for frozen balances.
* @param j Journal for trace logging.
* @param includeFullBalance Whether to include borrowable credit or max
* issuance capacity; defaults to `SimpleBalance`.
* @return The spendable balance, which may be negative (e.g. trust-line debt).
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -137,6 +360,19 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of an IOU for @p account.
*
* Convenience adapter over the `(Currency, AccountID)` overload, extracting
* the currency and issuer from @p issue.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param issue The IOU (currency + issuer).
* @param zeroIfFrozen Whether to return zero for frozen balances.
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable balance from @p account's perspective.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -146,6 +382,29 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of an MPT for @p account.
*
* - For the MPT issuer with `shFULL_BALANCE`: returns
* `MaximumAmount - OutstandingAmount` (available issuance capacity) via
* `availableMPTAmount`.
* - For regular holders: reads `sfMPTAmount` from the `MPToken` SLE. Returns
* zero if: the `MPToken` SLE does not exist; the token is frozen and
* `ZeroIfFrozen` is set; or the token is unauthorized and
* `ZeroIfUnauthorized` is set (with `featureSingleAssetVault` gating the
* precise auth-check path).
* - Under `featureMPTokensV2`, the result passes through
* `view.balanceHookMPT` to allow `PaymentSandbox` deferred-credit
* interception.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param mptIssue The MPT issuance.
* @param zeroIfFrozen Whether to zero the balance when frozen/locked.
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized.
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable MPT balance, or zero per the policy flags above.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -156,6 +415,22 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of any asset for @p account.
*
* Asset-dispatching overload. Delegates to the `Issue` overload (which
* ignores `zeroIfUnauthorized`) or the `MPTIssue` overload based on the
* runtime type of @p asset.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param asset The asset to query.
* @param zeroIfFrozen Whether to zero the balance when frozen.
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized
* (MPT only; ignored for IOU).
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable balance per the policy flags.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -166,11 +441,29 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much
// an account can spend while also allowing currency issuers to spend
// unlimited amounts of their own currency (since they can always issue more).
/** Return how much of @p saDefault's currency @p id can fund, treating the
* issuer as having unlimited supply of their own currency.
*
* For IOU: if `id == saDefault.getIssuer()`, returns `saDefault` directly —
* the issuer can always fund an offer for their own currency up to whatever
* amount they specify. Otherwise delegates to `accountHolds` with
* `SimpleBalance`.
*
* This is the correct semantic for offer matching; prefer `accountFunds` over
* `accountHolds` when asking "can this account fund this offer?".
*
* @note `saDefault` must hold an `Issue` (not MPT). Use the `AuthHandling`
* overload for asset-agnostic callers.
*
* @param view Read-only ledger view.
* @param id The account to query.
* @param saDefault The amount (currency + issuer) to check fundability
* for.
* @param freezeHandling Whether to zero the balance when frozen.
* @param j Journal for trace logging.
* @return `saDefault` if @p id is the issuer; otherwise the trust-line
* balance, zeroed per @p freezeHandling.
*/
[[nodiscard]] STAmount
accountFunds(
ReadView const& view,
@@ -179,7 +472,22 @@ accountFunds(
FreezeHandling freezeHandling,
beast::Journal j);
// Overload with AuthHandling to support IOU and MPT.
/** Asset-agnostic overload of `accountFunds` supporting both IOU and MPT.
*
* For IOU: delegates to the `FreezeHandling`-only overload above.
* For MPT: delegates to `accountHolds` with `shFULL_BALANCE`, which
* returns the issuer's available issuance capacity or the holder's
* `sfMPTAmount`.
*
* @param view Read-only ledger view.
* @param id The account to query.
* @param saDefault The amount (currency/asset + issuer) to check.
* @param freezeHandling Whether to zero the balance when frozen.
* @param authHandling Whether to zero the balance when unauthorized (MPT
* only).
* @param j Journal for trace logging.
* @return The fundable balance per the policy flags.
*/
[[nodiscard]] STAmount
accountFunds(
ReadView const& view,
@@ -189,9 +497,15 @@ accountFunds(
AuthHandling authHandling,
beast::Journal j);
/** Returns the transfer fee as Rate based on the type of token
* @param view The ledger view
* @param amount The amount to transfer
/** Return the transfer fee for the asset embedded in @p amount.
*
* Dispatches on `amount.asset()`: for IOU, reads the issuer's transfer rate
* from their `AccountRoot`; for MPT, reads the `sfTransferFee` field from
* the `MPTokenIssuance` SLE. Both paths return a `Rate` (parts-per-billion).
*
* @param view Read-only ledger view.
* @param amount The amount whose asset determines which fee to look up.
* @return The transfer fee as a `Rate`, or `parityRate` if no fee is set.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, STAmount const& amount);
@@ -202,9 +516,42 @@ transferRate(ReadView const& view, STAmount const& amount);
//
//------------------------------------------------------------------------------
/** Check whether a new holding object (trust line or MPToken) can be created.
*
* For IOU: verifies that the issuer's `AccountRoot` has `lsfDefaultRipple`
* set; returns `terNO_RIPPLE` if not, `terNO_ACCOUNT` if the issuer does not
* exist, `tesSUCCESS` for XRP. For MPT: delegates to the MPT-specific check.
*
* @note This function is read-only (takes `ReadView`) and is intended to be
* called during `preflight`. Any transactor that calls `addEmptyHolding`
* in `doApply` must call this function in `preflight` first.
*
* @param view Read-only ledger view.
* @param asset The asset for which a holding would be created.
* @return `tesSUCCESS` if a holding can be added; `terNO_RIPPLE`,
* `terNO_ACCOUNT`, or an MPT-specific error otherwise.
*/
[[nodiscard]] TER
canAddHolding(ReadView const& view, Asset const& asset);
/** Create an empty holding object (trust line or MPToken) for @p accountID.
*
* Dispatches to `addEmptyHolding(…, Issue)` or `addEmptyHolding(…, MPTIssue)`
* based on the runtime type of @p asset. The holding is created with zero
* balance and consumes an owner-count reserve slot.
*
* @note The caller must have invoked `canAddHolding` in `preflight` with the
* same view and asset to validate preconditions before calling this.
*
* @param view Mutable ledger view.
* @param accountID The account that will hold the asset.
* @param priorBalance The account's XRP balance before this transaction,
* used to test reserve sufficiency.
* @param asset The asset to create a holding for.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -213,6 +560,21 @@ addEmptyHolding(
Asset const& asset,
beast::Journal journal);
/** Delete a zero-balance holding object (trust line or MPToken) for @p accountID.
*
* Dispatches to `removeEmptyHolding(…, Issue)` or
* `removeEmptyHolding(…, MPTIssue)` based on the runtime type of @p asset.
* The holding must have a zero balance; a non-zero balance returns
* `tecHAS_OBLIGATIONS`.
*
* @param view Mutable ledger view.
* @param accountID The account whose holding should be removed.
* @param asset The asset identifying the holding to remove.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
* non-zero; `tecOBJECT_NOT_FOUND` if no holding exists; or a `tec`/`tef`
* error from the type-specific leaf.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -226,6 +588,25 @@ removeEmptyHolding(
//
//------------------------------------------------------------------------------
/** Check whether @p account is authorized to hold or interact with @p asset.
*
* Dispatches to `requireAuth(…, Issue, …)` or `requireAuth(…, MPTIssue, …)`
* based on the runtime type of @p asset.
*
* - `StrongAuth`: verifies the holding object exists first; returns
* `tecNO_LINE` (IOU) or `tecNO_AUTH` (MPT) if absent.
* - `WeakAuth`: skips the existence check; returns success if authorization
* is not required even when no holding exists.
* - `Legacy`: maps to `StrongAuth` for MPT and `WeakAuth` for IOU to
* preserve historical behavior.
*
* @param view Read-only ledger view.
* @param asset The asset to check authorization for.
* @param account The account to check.
* @param authType Authorization strictness; defaults to `AuthType::Legacy`.
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE` depending on the asset
* type and authorization state.
*/
[[nodiscard]] TER
requireAuth(
ReadView const& view,
@@ -233,6 +614,20 @@ requireAuth(
AccountID const& account,
AuthType authType = AuthType::Legacy);
/** Check whether @p asset can be transferred from @p from to @p to.
*
* Dispatches to the IOU or MPT leaf. For IOU, checks rippling flags on the
* trustlines (returns `terNO_RIPPLE` if both sides block rippling). For MPT,
* checks `lsfMPTCanTransfer` on the issuance and the destination's
* authorization state.
*
* @param view Read-only ledger view.
* @param asset The asset to transfer.
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, or an asset-specific
* error (`terNO_RIPPLE`, `tecNO_AUTH`, etc.) otherwise.
*/
[[nodiscard]] TER
canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to);
@@ -242,14 +637,29 @@ canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, Acc
//
//------------------------------------------------------------------------------
// Direct send w/o fees:
// - Redeeming IOUs and/or sending sender's own IOUs.
// - Create trust line of needed.
// --> bCheckIssuer : normally require issuer to be involved.
// [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles.
/** Calls static directSendNoFeeIOU if saAmount represents Issue.
* Calls static directSendNoFeeMPT if saAmount represents MPTIssue.
/** Send @p saAmount directly without applying transfer fees or limit checks.
*
* Used for IOU redemption, intra-issuer transfers, and MPT moves where the
* issuer is one of the endpoints. Dispatches to `directSendNoFeeIOU` for
* IOU and `directSendNoFeeMPT` for MPT.
*
* For IOU, @p bCheckIssuer controls whether the function asserts that the
* issuer is one of the endpoints. For MPT, the issuer check is not performed
* (`bCheckIssuer` must be `false` for MPT).
*
* @note This function is intentionally **not** marked `[[nodiscard]]` for
* compatibility with `DirectStep.cpp`, which discards the return value in
* certain control paths. All other callers should inspect the result.
*
* @param view Mutable ledger view.
* @param uSenderID The sending account.
* @param uReceiverID The receiving account.
* @param saAmount The amount to send; its asset determines the dispatch.
* @param bCheckIssuer If `true` (IOU only), asserts that the issuer is one
* of the endpoints. Must be `false` for MPT.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
TER
directSendNoFee(
@@ -260,8 +670,30 @@ directSendNoFee(
bool bCheckIssuer,
beast::Journal j);
/** Calls static accountSendIOU if saAmount represents Issue.
* Calls static accountSendMPT if saAmount represents MPTIssue.
/** Send @p saAmount from @p from to @p to, applying transfer fees when
* applicable.
*
* This is the main asset-transfer entry point for transactors. Dispatches to
* `accountSendIOU` or `accountSendMPT` based on the asset type embedded in
* @p saAmount. Transfer fees are applied unless `WaiveTransferFee::Yes` is
* passed.
*
* The `allowOverflow` flag is forwarded to the MPT path only and controls
* whether `OutstandingAmount` may transiently exceed `MaximumAmount` during
* the two-phase issue-then-redeem structure used by the payment engine. Direct
* sends should use `AllowMPTOverflow::No`.
*
* @param view Mutable ledger view.
* @param from The sending account.
* @param to The receiving account.
* @param saAmount The amount to send.
* @param j Journal for trace/debug logging.
* @param waiveFee Whether to skip the transfer fee; defaults to `No`.
* @param allowOverflow Whether MPT OutstandingAmount may transiently exceed
* MaximumAmount; defaults to `No`. Use `Yes` only in payment-engine
* routing.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
accountSend(
@@ -273,12 +705,34 @@ accountSend(
WaiveTransferFee waiveFee = WaiveTransferFee::No,
AllowMPTOverflow allowOverflow = AllowMPTOverflow::No);
/** A vector of (receiver, amount) pairs used by `accountSendMulti`. */
using MultiplePaymentDestinations = std::vector<std::pair<AccountID, Number>>;
/** Like accountSend, except one account is sending multiple payments (with the
* same asset!) simultaneously
/** Send the same @p asset from @p senderID to multiple @p receivers in one
* atomic operation.
*
* Calls static accountSendMultiIOU if saAmount represents Issue.
* Calls static accountSendMultiMPT if saAmount represents MPTIssue.
* Dispatches to `accountSendMultiIOU` or `accountSendMultiMPT` based on
* @p asset. Batching avoids repeated round-trips through the ledger state for
* the sender's balance and the issuance's `OutstandingAmount` field.
*
* For MPT, the `fixSecurity3_1_3` amendment switches the aggregate
* `MaximumAmount` check from a per-iteration stale-snapshot check (pre-fix)
* to an exact `uint64_t` running-total check (post-fix) to prevent precision
* loss at 19-digit magnitudes near `kMAX_MP_TOKEN_AMOUNT`.
*
* @note `receivers.size()` must be greater than 1 (asserted).
*
* @param view Mutable ledger view.
* @param senderID The account sending the asset.
* @param asset The asset to send (must match the type of all receiver
* amounts).
* @param receivers List of (AccountID, Number) destination pairs. All amounts
* must be non-negative. Sender-equals-receiver entries are silently
* skipped.
* @param j Journal for trace/debug logging.
* @param waiveFee Whether to skip transfer fees; defaults to `No`.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
accountSendMulti(
@@ -289,6 +743,23 @@ accountSendMulti(
beast::Journal j,
WaiveTransferFee waiveFee = WaiveTransferFee::No);
/** Transfer XRP directly between two accounts without reserve or fee checks.
*
* XRP has no trust lines, no transfer fees, and no authorization model, so
* it bypasses the Asset-dispatch path entirely. Both @p from and @p to must
* be non-zero and distinct. Returns `telFAILED_PROCESSING` (open ledger) or
* `tecFAILED_PROCESSING` (closed ledger) if the sender's balance is
* insufficient.
*
* @param view Mutable ledger view.
* @param from The sending account; must not be `beast::kZERO`.
* @param to The receiving account; must not be `beast::kZERO`.
* @param amount The XRP amount to transfer; must be native (XRP).
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `telFAILED_PROCESSING` or
* `tecFAILED_PROCESSING` if balance is insufficient; `tefINTERNAL` if
* either account SLE cannot be found.
*/
[[nodiscard]] TER
transferXRP(
ApplyView& view,

View File

@@ -1,3 +1,16 @@
/** @file
* Pure arithmetic helpers for the XLS-65d Single-Sided Vault feature.
*
* Each function converts between the two token types a vault manages:
* the underlying *asset* (XRP, IOU, or MPT that depositors contribute) and
* vault *shares* (an MPT representing proportional ownership). Because MPT
* values are always integers every function makes an explicit rounding
* decision — and those decisions differ between the deposit and withdrawal
* paths to protect vault solvency.
*
* These functions are stateless and side-effect-free; all ledger mutations
* are the caller's responsibility.
*/
#pragma once
#include <xrpl/protocol/STAmount.h>
@@ -8,53 +21,105 @@
namespace xrpl {
/** From the perspective of a vault, return the number of shares to give
depositor when they offer a fixed amount of assets. Note, since shares are
MPT, this number is integral and always truncated in this calculation.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param assets The amount of assets to convert.
@return The number of shares, or nullopt on error.
*/
/** Compute the shares minted when a depositor offers a fixed asset amount.
*
* Uses `sfAssetsTotal` from `vault` directly, *without* subtracting
* `sfLossUnrealized`. Unrealized losses are a risk borne by existing
* shareholders, not a discount for new depositors.
*
* **Bootstrap case**: when `sfAssetsTotal == 0` the result is
* `assets × 10^sfScale` (truncated), establishing the initial exchange rate.
* The non-bootstrap result is `(sfOutstandingAmount × assets) / sfAssetsTotal`,
* always truncated — depositors always receive a whole number of shares, never
* more than the assets strictly warrant.
*
* @note The deposit transactor calls this first, then back-calculates the
* true asset cost via `sharesToAssetsDeposit()` to ensure it never
* extracts more than the depositor offered.
* @throws std::overflow_error if `sfScale` is large enough to overflow
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
* `sfScale`, and `sfShareMPTID`.
* @param issuance The MPTokenIssuance SLE for the vault's share token;
* must contain `sfOutstandingAmount`.
* @param assets The asset amount to convert; must be non-negative and
* must match `vault->at(sfAsset)`.
* @return The integral share amount, or `nullopt` if `assets` is negative
* or its asset type does not match the vault.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets);
/** From the perspective of a vault, return the number of assets to take from
depositor when they receive a fixed amount of shares. Note, since shares are
MPT, they are always an integral number.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param shares The amount of shares to convert.
@return The number of assets, or nullopt on error.
*/
/** Compute the asset cost for a depositor who will receive a fixed share amount.
*
* This is the inverse of `assetsToSharesDeposit()` and is used in the second
* step of the deposit calculation: after truncating the forward direction to
* determine how many whole shares are created, the transactor calls this
* function to derive the exact asset amount to collect.
*
* Uses `sfAssetsTotal` directly, without subtracting `sfLossUnrealized`,
* matching the deposit-path convention.
*
* **Bootstrap case**: when `sfAssetsTotal == 0` the result uses `sfScale` to
* reverse the bootstrap formula applied by `assetsToSharesDeposit()`.
*
* @throws std::overflow_error if `sfScale` is large enough to overflow
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param shares The share amount to convert; must be non-negative and must
* match `vault->at(sfShareMPTID)`.
* @return The asset amount, or `nullopt` if `shares` is negative or its
* asset type does not match the vault's share MPT.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares);
/** Controls whether to truncate shares instead of rounding. */
/** Controls whether to truncate (floor) the share result instead of rounding.
*
* `No` (the default) rounds to nearest, ensuring the vault is never
* shortchanged when computing shares to redeem for a fixed asset withdrawal.
* `Yes` applies floor truncation, used when the caller explicitly needs
* conservative (depositor-favoring) rounding.
*/
enum class TruncateShares : bool { No = false, Yes = true };
/** From the perspective of a vault, return the number of shares to demand from
the depositor when they ask to withdraw a fixed amount of assets. Since
shares are MPT this number is integral, and it will be rounded to nearest
unless explicitly requested to be truncated instead.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param assets The amount of assets to convert.
@param truncate Whether to truncate instead of rounding.
@return The number of shares, or nullopt on error.
*/
/** Compute the shares a withdrawer must redeem to receive a fixed asset amount.
*
* Unlike the deposit path, this function subtracts `sfLossUnrealized` from
* `sfAssetsTotal` before computing the exchange rate. Withdrawers receive fewer
* assets per share when the vault has recorded unrealized losses, preventing
* early withdrawers from exiting at inflated prices at the expense of remaining
* holders.
*
* The result is rounded to nearest by default (`TruncateShares::No`), ensuring
* the vault is not shortchanged. The withdraw transactor then back-calculates
* the actual assets delivered via `sharesToAssetsWithdraw()` for a precise
* two-step computation.
*
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
* a zero-valued `STAmount` rather than dividing by zero.
*
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
* callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
* `sfLossUnrealized`, and `sfShareMPTID`.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param assets The asset amount to convert; must be non-negative and must
* match `vault->at(sfAsset)`.
* @param truncate Whether to truncate instead of rounding to nearest.
* @return The integral share amount, or `nullopt` if `assets` is negative or
* its asset type does not match the vault.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
@@ -62,16 +127,25 @@ assetsToSharesWithdraw(
STAmount const& assets,
TruncateShares truncate = TruncateShares::No);
/** From the perspective of a vault, return the number of assets to give the
depositor when they redeem a fixed amount of shares. Note, since shares are
MPT, they are always an integral number.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param shares The amount of shares to convert.
@return The number of assets, or nullopt on error.
*/
/** Compute the assets delivered when a withdrawer redeems a fixed share amount.
*
* Like `assetsToSharesWithdraw()`, this function subtracts `sfLossUnrealized`
* from `sfAssetsTotal` before computing the exchange rate, so withdrawers
* bear their proportional share of any recorded losses.
*
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
* a zero-valued `STAmount` rather than dividing by zero.
*
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
* callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param shares The share amount to convert; must be non-negative and must
* match `vault->at(sfShareMPTID)`.
* @return The asset amount, or `nullopt` if `shares` is negative or its
* asset type does not match the vault's share MPT.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,

View File

@@ -2,20 +2,57 @@
namespace xrpl {
/**
* @brief Enumeration of ledger shortcuts for specifying which ledger to use.
/** Symbolic names for the three canonical XRPL ledger states.
*
* These shortcuts provide a convenient way to reference commonly used ledgers
* without needing to specify their exact hash or sequence number.
* The XRPL consensus model maintains three distinct ledger states at any
* point in time. Rather than requiring callers to pass magic strings
* (`"current"`, `"closed"`, `"validated"`) or ad-hoc integer sentinels,
* `LedgerShortcut` gives the type system a precise vocabulary for expressing
* ledger-selection intent without a specific sequence number or hash.
*
* In `RPCLedgerHelpers.cpp`, `lookupLedger` parsing maps the JSON strings
* `"current"`, `"closed"`, and `"validated"` onto the corresponding enum
* values before dispatching to the appropriate `getLedger` overload. The
* `AccountTx` RPC handler performs the same mapping when processing the
* `ledger_index` field. The gRPC adapter maps protobuf shortcut constants to
* these values as well.
*
* `LedgerShortcut` also participates as one arm of
* `RelationalDatabase::LedgerSpecifier` — a
* `std::variant<LedgerRange, LedgerShortcut, LedgerSequence, LedgerHash>` —
* allowing symbolic ledger names to flow through the database query layer via
* `std::visit` dispatch without special-case handling.
*
* @note The scoped `enum class` form prevents implicit integer conversions and
* namespace pollution, both of which are hazards in a codebase that also
* works extensively with raw integer ledger sequence numbers.
*/
enum class LedgerShortcut {
/** The current working ledger (open, not yet closed) */
/** The open, in-progress ledger still accumulating new transactions.
*
* This ledger has not been closed or validated, so its contents may
* change. Results derived from it are not final and may be rolled back
* during a reorganisation or consensus failure.
*/
Current,
/** The most recently closed ledger (may not be validated) */
/** The most recently closed ledger; stable in structure but not yet
* consensus-validated.
*
* No new transactions are accepted into this ledger, but the network has
* not yet confirmed it as the authoritative chain tip. It is more stable
* than `Current` but still not suitable for finality guarantees.
*/
Closed,
/** The most recently validated ledger */
/** The most recently validated ledger; the fully consensus-confirmed chain
* tip.
*
* This is the only state considered immutable and trustworthy for finality
* purposes. An RPC node that cannot provide a fresh validated ledger
* (i.e., it is stale) will return an error rather than serve potentially
* incorrect data.
*/
Validated
};

View File

@@ -1,3 +1,8 @@
/** @file
* Defines MPTAmount, the canonical signed-integer amount type for
* Multi-Purpose Tokens (MPTs) on the XRP Ledger.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -13,12 +18,40 @@
namespace xrpl {
/** Typed signed-integer quantity for Multi-Purpose Tokens (MPTs).
*
* MPT balances are plain whole-unit integers — no mantissa/exponent pair,
* no sub-unit naming — capped at `maxMPTokenAmount` (INT64_MAX) by the
* protocol. The class sits alongside `XRPAmount` and `IOUAmount` as one
* of the three concrete amount types that satisfy the `StepAmount` concept
* used by the payment-path and DEX engines.
*
* Arithmetic operators are composed via Boost.Operators (CRTP):
* - `boost::totally_ordered<MPTAmount>` — synthesizes `!=`, `>`, `>=`,
* `<=` from the declared `==` and `<`.
* - `boost::additive<MPTAmount>` — synthesizes binary `+`/`-` from
* `+=`/`-=`.
* - `boost::equality_comparable<MPTAmount, int64_t>` — heterogeneous `!=`
* from `operator==(value_type)`.
* - `boost::additive<MPTAmount, int64_t>` — heterogeneous `+`/`-` with
* raw integers.
*
* Out-of-line `+=`, `-=`, `operator-()`, `==`, and `<` perform no overflow
* detection; callers are responsible for keeping balances in range through
* the ledger constraint machinery. The safe multiplication path
* (`mulRatio`) uses 128-bit intermediates and throws on overflow.
*
* @note `value_` is `protected` (not `private`) to allow subclassing
* without exposing the raw integer to unrelated code. No subclasses
* exist in the current codebase.
*/
class MPTAmount : private boost::totally_ordered<MPTAmount>,
private boost::additive<MPTAmount>,
private boost::equality_comparable<MPTAmount, std::int64_t>,
private boost::additive<MPTAmount, std::int64_t>
{
public:
/** Underlying integer type; matches `XRPAmount::value_type`. */
using value_type = std::int64_t;
protected:
@@ -27,57 +60,149 @@ protected:
public:
MPTAmount() = default;
constexpr MPTAmount(MPTAmount const& other) = default;
/** Construct a zero amount from the `beast::Zero` sentinel.
*
* Allows idiomatic zero-initialization via `beast::zero` in generic
* code that is templated on amount type.
*/
constexpr MPTAmount(beast::Zero);
constexpr MPTAmount&
operator=(MPTAmount const& other) = default;
// Round to nearest, even on tie.
/** Construct from a `Number`, rounding to nearest with ties to even.
*
* Provides implicit compatibility with XRPL's high-precision arithmetic
* type. The rounding mode matches IEEE 754 default (round-half-to-even).
*
* @param x The `Number` value to convert.
*/
explicit MPTAmount(Number const& x) : MPTAmount(static_cast<value_type>(x))
{
}
/** Construct from a raw `int64_t` value.
*
* Explicit to prevent accidental implicit conversion from integers.
* The caller is responsible for ensuring `value` does not exceed
* `maxMPTokenAmount` (INT64_MAX).
*
* @param value The integer amount in whole MPT units.
*/
constexpr explicit MPTAmount(value_type value);
/** Assign the `beast::Zero` sentinel, setting the amount to zero. */
constexpr MPTAmount& operator=(beast::Zero);
/** Add `other` to this amount in place.
*
* No overflow detection is performed; callers must ensure the result
* remains within `int64_t` range.
*
* @param other The amount to add.
* @return Reference to `*this` after addition.
*/
MPTAmount&
operator+=(MPTAmount const& other);
/** Subtract `other` from this amount in place.
*
* No overflow detection is performed; callers must ensure the result
* remains within `int64_t` range.
*
* @param other The amount to subtract.
* @return Reference to `*this` after subtraction.
*/
MPTAmount&
operator-=(MPTAmount const& other);
/** Return the arithmetic negation of this amount.
*
* Used where a credit and a debit are expressed as equal-magnitude
* amounts of opposite sign before being applied to the ledger.
* Negating `INT64_MIN` is undefined behavior; callers must avoid it.
*
* @return A new `MPTAmount` equal to `-value_`.
*/
MPTAmount
operator-() const;
/** Test equality with another `MPTAmount`.
*
* Together with `operator<`, satisfies `boost::totally_ordered`,
* from which `!=`, `>`, `<=`, and `>=` are synthesized.
*
* @param other The amount to compare against.
* @return `true` if both amounts hold the same integer value.
*/
bool
operator==(MPTAmount const& other) const;
/** Test equality with a raw `int64_t` value.
*
* Allows expressions like `amt == 0` without constructing a temporary.
* `boost::equality_comparable<MPTAmount, int64_t>` synthesizes the
* mixed-type `!=` from this overload.
*
* @param other The raw integer value to compare against.
* @return `true` if `value_` equals `other`.
*/
bool
operator==(value_type other) const;
/** Return `true` if this amount is strictly less than `other`.
*
* The single total-order primitive from which `boost::totally_ordered`
* derives `>`, `<=`, and `>=`. Signed comparison gives correct
* semantics for negative balances.
*
* @param other The amount to compare against.
* @return `true` if `value_` is strictly less than `other.value_`.
*/
bool
operator<(MPTAmount const& other) const;
/** Returns true if the amount is not zero */
/** Returns true if the amount is not zero. */
explicit constexpr
operator bool() const noexcept;
/** Implicit conversion to `Number` for use in high-precision arithmetic.
*
* Allows `MPTAmount` to be passed anywhere a `Number` is expected —
* arithmetic operations, rounding, and comparisons — without an explicit
* cast. The reverse direction (construction from `Number`) is explicit.
*/
operator Number() const noexcept
{
return value();
}
/** Return the sign of the amount */
/** Return the sign of the amount.
*
* @return `-1` if negative, `0` if zero, `1` if positive.
*/
[[nodiscard]] constexpr int
signum() const noexcept;
/** Returns the underlying value. Code SHOULD NOT call this
function unless the type has been abstracted away,
e.g. in a templated function.
*/
/** Return the underlying integer value.
*
* Code SHOULD NOT call this function unless the type has been abstracted
* away, e.g. in a templated function. Prefer operating on `MPTAmount`
* directly to keep arithmetic in the typed domain.
*
* @return The raw `int64_t` balance in whole MPT units.
*/
[[nodiscard]] constexpr value_type
value() const;
/** Return the smallest positive MPT amount (one indivisible unit).
*
* Provides a uniform factory interface shared with `XRPAmount` and
* `IOUAmount` so generic payment-path code can obtain the minimum
* step size without knowing the concrete amount type.
*
* @return `MPTAmount{1}`.
*/
static MPTAmount
minPositiveAmount();
};
@@ -98,14 +223,12 @@ MPTAmount::operator=(beast::Zero)
return *this;
}
/** Returns true if the amount is not zero */
constexpr MPTAmount::
operator bool() const noexcept
{
return value_ != 0;
}
/** Return the sign of the amount */
constexpr int
MPTAmount::signum() const noexcept
{
@@ -114,17 +237,13 @@ MPTAmount::signum() const noexcept
return (value_ != 0) ? 1 : 0;
}
/** Returns the underlying value. Code SHOULD NOT call this
function unless the type has been abstracted away,
e.g. in a templated function.
*/
constexpr MPTAmount::value_type
MPTAmount::value() const
{
return value_;
}
// Output MPTAmount as just the value.
/** Stream an `MPTAmount` as its raw integer value. */
template <class Char, class Traits>
std::basic_ostream<Char, Traits>&
operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
@@ -132,12 +251,35 @@ operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
return os << q.value();
}
/** Return the decimal string representation of an `MPTAmount`. */
inline std::string
to_string(MPTAmount const& amount)
{
return std::to_string(amount.value());
}
/** Compute `amt * num / den` with configurable rounding direction.
*
* The intermediate product is computed in 128-bit arithmetic to avoid
* overflow when multiplying a 63-bit MPT balance by a 32-bit numerator
* (up to 95 bits required). After division, any remainder is resolved
* based on the sign of `amt` and `roundUp`:
* - Positive amounts round up when `roundUp` is `true`.
* - Negative amounts round away from zero (more negative) when `roundUp`
* is `false`.
*
* Used for fee and reserve calculations that apply percentage-style ratios
* to MPT amounts.
*
* @param amt The base amount to scale.
* @param num Numerator of the ratio (32-bit unsigned).
* @param den Denominator of the ratio (32-bit unsigned, must be > 0).
* @param roundUp If `true`, round the result toward positive infinity;
* if `false`, round toward negative infinity.
* @return The scaled `MPTAmount`.
* @throws std::runtime_error If `den` is zero.
* @throws std::overflow_error If the result exceeds `INT64_MAX`.
*/
inline MPTAmount
mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp)
{

View File

@@ -5,9 +5,23 @@
namespace xrpl {
/* Adapt MPTID to provide the same interface as Issue. Enables using static
* polymorphism by Asset and other classes. MPTID is a 192-bit concatenation
* of a 32-bit account sequence and a 160-bit account id.
/** Identifies a Multi-Purpose Token issuance, adapting `MPTID` to mirror
* the public interface of `Issue`.
*
* `MPTIssue` wraps a single 192-bit `MPTID` (32-bit big-endian sequence
* concatenated with a 160-bit `AccountID`) and exposes the same accessors as
* `Issue` — `getIssuer()`, `getText()`, `setJson()`, `native()`, and
* `integral()` — allowing `Asset` and any template code constrained by
* `ValidIssueType` to treat MPTs and IOUs uniformly.
*
* Key semantic differences from `Issue`:
* - `native()` always returns `false` (MPTs are never the native currency).
* - `integral()` always returns `true` (MPT amounts are 64-bit integers,
* unlike IOUs which use multi-precision rational arithmetic).
* - Equality and ordering compare the full 192-bit `MPTID`, with no
* special-case equivalence class for any sentinel value.
*
* @see Issue, Asset, MPTID
*/
class MPTIssue
{
@@ -17,27 +31,80 @@ private:
public:
MPTIssue() = default;
/** Constructs an MPTIssue from a pre-formed 192-bit issuance identifier.
*
* @param issuanceID The packed MPTID (32-bit sequence ‖ 160-bit AccountID).
*/
MPTIssue(MPTID const& issuanceID);
/** Constructs an MPTIssue from the raw components of an MPTID.
*
* Delegates to `xrpl::makeMptID(sequence, account)` to assemble the
* packed MPTID, saving callers from invoking that helper explicitly.
*
* @param sequence The issuer's account sequence number at the time of
* issuance.
* @param account The AccountID of the issuer.
*/
MPTIssue(std::uint32_t sequence, AccountID const& account);
/** Implicit conversion to the underlying `MPTID`.
*
* Allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected
* without an explicit cast.
*
* @return A reference to the underlying `MPTID`, valid for the lifetime
* of this object.
*/
operator MPTID const&() const
{
return mptID_;
}
/** Extracts the issuer's `AccountID` from the packed `MPTID`.
*
* `MPTID` lays out 4 bytes of sequence followed immediately by 20 bytes
* of `AccountID`. This method returns a reference into that buffer by
* pointer-casting past the leading 4 bytes. A `static_assert` on the
* total size of `MPTID` guards against layout changes breaking this
* assumption at compile time.
*
* @return A reference to the `AccountID` embedded in the `MPTID`.
* Valid for the lifetime of this `MPTIssue` object.
* @note The free function `getMPTIssuer()` achieves the same extraction
* via `std::bit_cast` and returns by value, avoiding any
* lifetime dependency on the source object.
*/
[[nodiscard]] AccountID const&
getIssuer() const;
/** Returns the underlying `MPTID`.
*
* @return A reference to the 192-bit issuance identifier, valid for the
* lifetime of this object.
*/
[[nodiscard]] constexpr MPTID const&
getMptID() const
{
return mptID_;
}
/** Returns the hex string representation of the underlying `MPTID`.
*
* @return Uppercase hex encoding of the 192-bit issuance identifier.
*/
[[nodiscard]] std::string
getText() const;
/** Writes the issuance identifier into a JSON object under the key
* `mpt_issuance_id`.
*
* Serializes the `MPTID` as a hex string. Contrasted with
* `Issue::setJson()`, which writes separate `currency` and `issuer` keys.
*
* @param jv Output JSON object to write into; existing keys are not
* cleared.
*/
void
setJson(json::Value& jv) const;
@@ -47,12 +114,30 @@ public:
friend constexpr std::weak_ordering
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs);
/** Returns `false`; MPTs are never the native asset (XRP).
*
* Mirrors `Issue::native()` so that generic code can query XRP-ness
* without a type dispatch. `Asset::getAmountType()` relies on this flag
* to select `XRPAmount` vs `IOUAmount` vs `MPTAmount` at compile time.
*
* @return Always `false`.
*/
static bool
native()
{
return false;
}
/** Returns `true`; MPT amounts are stored as 64-bit integers.
*
* Mirrors the naming of `Issue::integral()` so that generic code can
* distinguish integer (drop/MPT) amounts from multi-precision IOU
* amounts without a type dispatch. Unlike `Issue::integral()`, this is
* unconditionally `true` — all MPTs use integer arithmetic regardless of
* the token configuration.
*
* @return Always `true`.
*/
static bool
integral()
{
@@ -60,19 +145,44 @@ public:
}
};
/** Returns `true` if two `MPTIssue` instances represent the same issuance.
*
* Delegates to the full 192-bit comparison of the underlying `MPTID`s.
* Both the sequence number and the issuer `AccountID` must match; there is
* no partial-equality exemption as there is in `Issue::operator==` for XRP.
*
* @param lhs Left-hand issuance.
* @param rhs Right-hand issuance.
* @return `true` iff both `MPTID`s are bitwise equal.
*/
constexpr bool
operator==(MPTIssue const& lhs, MPTIssue const& rhs)
{
return lhs.mptID_ == rhs.mptID_;
}
/** Provides a strict weak ordering over `MPTIssue` values.
*
* Delegates to the 192-bit lexicographic comparison of the underlying
* `MPTID`s. The ordering is consistent with `operator==`: two issuances are
* equivalent iff their full `MPTID`s are identical.
*
* @param lhs Left-hand issuance.
* @param rhs Right-hand issuance.
* @return A `std::weak_ordering` value consistent with `operator==`.
*/
constexpr std::weak_ordering
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs)
{
return lhs.mptID_ <=> rhs.mptID_;
}
/** MPT is a non-native token.
/** Returns `false`; an `MPTID` never identifies the native XRP asset.
*
* Provides the same naming convention as `isXRP(Issue)`, allowing call
* sites to test XRP-ness uniformly across both issue types.
*
* @return Always `false`.
*/
inline bool
isXRP(MPTID const&)
@@ -80,6 +190,21 @@ isXRP(MPTID const&)
return false;
}
/** Extracts the issuer `AccountID` from a `MPTID` by value.
*
* Copies the 20 bytes that follow the leading 4-byte sequence field into a
* temporary array, then uses `std::bit_cast` to reinterpret them as an
* `AccountID`. The `static_assert` on the total size of `MPTID` ensures the
* layout assumption holds; if the type ever gains padding the build fails.
* `std::bit_cast` is typically optimized to nothing in the final assembly.
*
* @param mptid The 192-bit issuance identifier to extract from.
* @return The `AccountID` embedded in bytes 423 of `mptid`.
* @note Use `MPTIssue::getIssuer()` when a zero-copy reference into an
* existing `MPTIssue` object is sufficient. The rvalue overloads of
* this function are deleted to prevent dangling references from
* temporaries.
*/
inline AccountID
getMPTIssuer(MPTID const& mptid)
{
@@ -93,12 +218,21 @@ getMPTIssuer(MPTID const& mptid)
return std::bit_cast<AccountID>(bytes);
}
// Disallow temporary
// Deleted to prevent a dangling-reference bug: if a temporary MPTID were
// accepted, the returned AccountID const& would immediately dangle.
AccountID const&
getMPTIssuer(MPTID const&&) = delete;
AccountID const&
getMPTIssuer(MPTID&&) = delete;
/** Returns the `MPTID` sentinel representing "no MPT".
*
* Encodes `{ sequence=0, account=noAccount() }` — all-zero bits.
* Mirrors `noIssue()` in `Issue.h` for use in contexts where a
* missing or invalid MPT must be represented without `std::optional`.
*
* @return The all-zero 192-bit sentinel `MPTID`.
*/
inline MPTID
noMPT()
{
@@ -106,6 +240,15 @@ noMPT()
return kMPT.getMptID();
}
/** Returns the `MPTID` sentinel representing a structurally invalid MPT.
*
* Encodes `{ sequence=0, account=xrpAccount() }` — sequence zero with the
* XRP account address as issuer, which is a conventionally invalid issuer
* for MPTs. `Asset`'s `BadAsset` comparison detects this sentinel by
* checking `getIssuer() == xrpAccount()`.
*
* @return The sentinel `MPTID` whose issuer is `xrpAccount()`.
*/
inline MPTID
badMPT()
{
@@ -113,6 +256,15 @@ badMPT()
return kMPT.getMptID();
}
/** Appends the underlying `MPTID` to a hasher.
*
* Plugs `MPTIssue` into the Beast hashing framework, enabling use in
* Beast-aware hash maps and sets.
*
* @tparam Hasher A type satisfying the `beast::hash_append` concept.
* @param h The hasher to append to.
* @param r The issuance whose `MPTID` is appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, MPTIssue const& r)
@@ -121,15 +273,49 @@ hash_append(Hasher& h, MPTIssue const& r)
hash_append(h, r.getMptID());
}
/** Returns the canonical wire-format JSON representation of an MPT issuance.
*
* Convenience wrapper around `MPTIssue::setJson()`. The returned object
* contains a single `mpt_issuance_id` field with the hex-encoded `MPTID`.
*
* @param mptIssue The issuance to serialize.
* @return A JSON object of the form `{"mpt_issuance_id": "<hex>"}`.
*/
json::Value
toJson(MPTIssue const& mptIssue);
/** Returns the hex string representation of an MPT issuance.
*
* @param mptIssue The issuance to convert.
* @return Uppercase hex encoding of the underlying 192-bit `MPTID`.
*/
std::string
to_string(MPTIssue const& mptIssue);
/** Parses an MPT issuance from a JSON object.
*
* Validates in strict order: `v` must be a JSON object; `currency` and
* `issuer` keys must be absent (their presence indicates IOU data routed
* to the wrong parser); `mpt_issuance_id` must be a string containing a
* valid 48-character hex-encoded `MPTID`.
*
* @param jv The JSON value to parse; must be an object.
* @return The parsed `MPTIssue`.
* @throws std::runtime_error if `jv` is not a JSON object, or if `currency`
* or `issuer` keys are present.
* @throws json::Error if `mpt_issuance_id` is absent, not a string, or not
* a valid 192-bit hex value.
* @see toJson for the inverse operation.
*/
MPTIssue
mptIssueFromJson(json::Value const& jv);
/** Writes the hex representation of an MPT issuance to a stream.
*
* @param os The output stream.
* @param x The issuance to write.
* @return `os`, to allow chaining.
*/
std::ostream&
operator<<(std::ostream& os, MPTIssue const& x);
@@ -137,6 +323,13 @@ operator<<(std::ostream& os, MPTIssue const& x);
namespace std {
/** Specializes `std::hash` for `xrpl::MPTID`, delegating to the type's own
* hasher.
*
* Enables `MPTID` to be used directly as a key in `std::unordered_map`,
* `std::unordered_set`, and similar standard containers without wrapping
* in `MPTIssue`.
*/
template <>
struct hash<xrpl::MPTID> : xrpl::MPTID::hasher
{

View File

@@ -1,3 +1,12 @@
/** @file
* Holds one pre-built `Json::Value` per supported API version so that a
* single ledger event can be delivered to subscribers speaking different API
* versions without re-serializing on every send.
*
* The public alias `xrpl::MultiApiJson` binds the template to the live
* version range `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -14,6 +23,15 @@
namespace xrpl {
namespace detail {
/** Variable template that is `true` only for lvalue-reference-qualified
* `std::integral_constant` specializations (both cv-variants).
*
* Used as the building block for the `some_integral_constant` concept, which
* disambiguates the compile-time and runtime overloads of `VisitorT::operator()`.
*
* @tparam T The type to test.
*/
template <typename T>
constexpr bool kIS_INTEGRAL_CONSTANT = false;
template <typename I, auto A>
@@ -21,35 +39,94 @@ constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A>&> = true;
template <typename I, auto A>
constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A> const&> = true;
/** Concept satisfied only by `std::integral_constant` specializations (lvalue refs).
*
* Used in `requires` clauses on `VisitorT::operator()` to prevent the
* runtime-`unsigned` overloads from being selected when a compile-time
* constant is passed, avoiding otherwise-ambiguous partial ordering.
*
* @tparam T The type to constrain.
*/
template <typename T>
concept some_integral_constant = detail::kIS_INTEGRAL_CONSTANT<T&>;
// This class is designed to wrap a collection of _almost_ identical json::Value
// objects, indexed by version (i.e. there is some mapping of version to object
// index). It is used e.g. when we need to publish JSON data to users supporting
// different API versions. We allow manipulation and inspection of all objects
// at once with `isMember` and `set`, and also individual inspection and updates
// of an object selected by the user by version, using `visitor_t` nested type.
/** Holds one `Json::Value` per API version in a fixed-size array, enabling
* single-pass event serialization for multi-version subscriber delivery.
*
* When an XRPL server event (e.g., a validated transaction) must be published
* to subscribers that may speak different API versions, re-serializing or
* branching inside the send path would add latency proportional to subscriber
* count. `MultiApiJson` amortizes version-specific transformations to once per
* event: callers construct the object from a common base `Json::Value`, apply
* per-version mutations via `visit`, and then each subscriber's delivery path
* calls `visit(apiVersion, sender)` to pick the pre-built slot cheaply.
*
* The array has `MaxVer + 1 - MinVer` elements; version `v` maps to index
* `v - MinVer`. `set` and `isMember` operate across all slots; `visit`
* operates on a single slot selected by version.
*
* @note Prefer the `xrpl::MultiApiJson` type alias over instantiating this
* template directly. Direct instantiation is intended for tests only; all
* production code should use the alias, which is bound to the live version
* constants and automatically tracks any future version-range changes.
*
* @tparam MinVer Minimum (inclusive) supported API version.
* @tparam MaxVer Maximum (inclusive) supported API version.
*/
template <unsigned MinVer, unsigned MaxVer>
struct MultiApiJson
{
static_assert(MinVer <= MaxVer);
/** Returns `true` if `v` falls within `[MinVer, MaxVer]`.
*
* Used by `VisitorT` to guard against out-of-range version accesses.
* @param v The API version number to test.
* @return `true` iff `v` is a valid slot index.
*/
static constexpr auto
valid(unsigned int v) noexcept -> bool
{
return v >= MinVer && v <= MaxVer;
}
/** Maps an API version number to its zero-based array slot.
*
* Out-of-range values below `MinVer` clamp to 0 rather than underflowing;
* the caller is responsible for checking `valid(v)` before trusting the
* result. Values above `MaxVer` are not clamped — `valid()` must be used
* to guard against those.
*
* @param v The API version number to map.
* @return The corresponding index into `val`.
*/
static constexpr auto
index(unsigned int v) noexcept -> std::size_t
{
return (v < MinVer) ? 0 : static_cast<std::size_t>(v - MinVer);
}
/** Number of API version slots stored; equals `MaxVer + 1 - MinVer`. */
constexpr static std::size_t kSIZE = MaxVer + 1 - MinVer;
/** The per-version JSON values, indexed by `index(version)`.
*
* Public to allow direct slot access in tests and for `VisitorT` (which
* is a friend via the `static constexpr` data member). Production callers
* should use `set`, `isMember`, and `visit` rather than indexing directly.
*/
std::array<json::Value, kSIZE> val = {};
/** Constructs the object, optionally copy-initializing every slot.
*
* When `init` is the default (null) `Json::Value`, all slots remain
* default-initialized (null). When a non-null value is supplied, every
* slot is copy-initialized to it. The common pattern in `NetworkOPs.cpp`
* is to pass a shared base object and then apply per-version mutations
* via `visit`.
*
* @param init Base value to copy into every slot; omit for null slots.
*/
explicit MultiApiJson(json::Value const& init = {})
{
if (init == json::Value{})
@@ -58,6 +135,16 @@ struct MultiApiJson
v = init;
}
/** Writes a key-value pair into every slot simultaneously.
*
* Use for fields that are identical across all API versions — the majority
* of transaction fields. Cheaper than calling `visit` once per version for
* shared data. The `requires` clause restricts `v` to types from which
* `Json::Value` can be constructed, preventing silent misuse.
*
* @param key The JSON object key to set.
* @param v The value to assign; must be constructible to `Json::Value`.
*/
void
set(char const* key, auto const& v)
requires std::constructible_from<json::Value, decltype(v)>
@@ -66,8 +153,26 @@ struct MultiApiJson
a[key] = v;
}
enum class IsMemberResult : int { None = 0, Some, All };
/** Tri-state result of `isMember`: indicates how many version slots contain a key.
*
* Scoped to `MultiApiJson` rather than a separate class enum deliberately —
* the struct is narrow enough to serve as its own scope for this result.
*/
enum class IsMemberResult : int {
None = 0, /**< No slot contains the key. */
Some, /**< At least one but not all slots contain the key. */
All /**< Every slot contains the key. */
};
/** Queries how many version slots contain the given JSON key.
*
* Useful for asserting that version-specific mutations were (or were not)
* applied before delivery. `NetworkOPs` uses it in assertions to verify
* that certain fields are never set on a freshly-constructed object.
*
* @param key The JSON object key to look up in each slot.
* @return `IsMemberResult::None`, `Some`, or `All`.
*/
[[nodiscard]] IsMemberResult
isMember(char const* key) const
{
@@ -83,6 +188,33 @@ struct MultiApiJson
return count < kSIZE ? IsMemberResult::Some : IsMemberResult::All;
}
/** Stateless callable that routes invocations to the correct version slot.
*
* Provides four `operator()` overloads split along two axes:
*
* 1. **Compile-time version** (`std::integral_constant<unsigned, V>`):
* the version is checked with `static_assert`; the JSON reference and
* optional extra arguments are forwarded to `fn` at compile time.
*
* 2. **Runtime version** (any type convertible to `unsigned` that is
* *not* an `integral_constant`): the version is checked with
* `XRPL_ASSERT`; the `some_integral_constant` concept in the `requires`
* clause prevents these overloads from being selected when a
* compile-time constant is passed, resolving the otherwise-ambiguous
* partial ordering.
*
* Each axis is further split by whether extra arguments are forwarded to
* `fn` after the `Json::Value` (and possibly the version value). This
* matches the calling convention of `forAllApiVersions`/`forApiVersions`,
* which pass each version as an `integral_constant` plus any extra args
* bound at the call site.
*
* `const`-propagation is automatic: the JSON reference passed to `fn`
* mirrors the `const`-ness of the `Json&` parameter.
*
* @note Exposed as `kVISITOR` to allow direct testing; prefer `visit()`
* for all production call sites.
*/
static constexpr struct VisitorT final
{
// integral_constant version, extra arguments
@@ -145,6 +277,19 @@ struct MultiApiJson
}
} kVISITOR = {};
/** Returns a closure that dispatches `kVISITOR` for this object (mutable).
*
* The returned callable captures `this` and forwards all arguments to
* `kVISITOR`. This form is composable with `forAllApiVersions` and
* `forApiVersions`: those utilities iterate the version range at compile
* time, passing each version as an `integral_constant`. The closure
* satisfies that calling convention exactly, so
* `forAllApiVersions(obj.visit(), lambda)` iterates every version with a
* single consistent lambda without any per-version conditional logic.
*
* @return A lambda `(auto... args) -> auto` that calls
* `kVISITOR(*this, args...)`.
*/
auto
visit()
{
@@ -155,6 +300,16 @@ struct MultiApiJson
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
}
/** Returns a closure that dispatches `kVISITOR` for this object (const).
*
* Identical to the mutable overload but captures `this` as `const`,
* propagating const-ness through to the `Json::Value` reference passed to
* the callable. Used when the caller only needs to read the pre-built JSON
* (e.g., subscriber delivery in `BookListeners::publish`).
*
* @return A lambda `(auto... args) -> auto` that calls
* `kVISITOR(*this, args...)` on the const object.
*/
[[nodiscard]] auto
visit() const
{
@@ -165,6 +320,20 @@ struct MultiApiJson
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
}
/** Directly invokes `kVISITOR` for a single version (mutable).
*
* Equivalent to `visit()(args...)` but avoids the closure allocation.
* Typical usage:
* ```cpp
* jvObj.visit(RPC::kAPI_VERSION<1>, [](Json::Value& jv) {
* jv["ledger_index"] = std::to_string(jv["ledger_index"].asInt());
* });
* ```
*
* @param args Version (compile-time or runtime) followed by a callable
* and any extra arguments accepted by `kVISITOR`.
* @return The return value of the callable.
*/
template <typename... Args>
auto
visit(Args... args) -> std::invoke_result_t<VisitorT, MultiApiJson&, Args...>
@@ -174,6 +343,20 @@ struct MultiApiJson
return kVISITOR(*this, std::forward<decltype(args)>(args)...);
}
/** Directly invokes `kVISITOR` for a single version (const).
*
* Const counterpart of the mutable `visit(args...)` overload. Used when
* the JSON slot must not be mutated — for example in the subscriber
* delivery path where each subscriber picks its pre-built slot:
* ```cpp
* jvObj.visit(subscriber->getApiVersion(),
* [&](Json::Value const& jv) { subscriber->send(jv, true); });
* ```
*
* @param args Version (compile-time or runtime) followed by a callable
* and any extra arguments accepted by `kVISITOR`.
* @return The return value of the callable.
*/
template <typename... Args>
[[nodiscard]] auto
visit(Args... args) const -> std::invoke_result_t<VisitorT, MultiApiJson const&, Args...>
@@ -186,7 +369,15 @@ struct MultiApiJson
} // namespace detail
// Wrapper for Json for all supported API versions.
/** Holds one pre-built `Json::Value` per currently supported API version.
*
* Bound to `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`
* (currently versions 13), so the concrete type stores exactly three
* `Json::Value` objects. Changing those constants automatically resizes
* every `MultiApiJson` instance in the server.
*
* @see detail::MultiApiJson for the full behavioral contract.
*/
using MultiApiJson =
detail::MultiApiJson<RPC::kAPI_MINIMUM_SUPPORTED_VERSION, RPC::kAPI_MAXIMUM_VALID_VERSION>;

View File

@@ -1,3 +1,20 @@
/** @file
* Aggregator entry point for injecting synthetic NFT fields into RPC
* transaction responses.
*
* "Synthetic" fields (`nftoken_ids`, `nftoken_id`, `offer_id`) are derived
* at query time from the ledger state changes recorded in `TxMeta`; they are
* not stored on-chain. Callers invoke a single function here rather than
* calling the individual NFT inserters directly, keeping call sites from
* accumulating an ever-growing list of per-type injector calls as new NFT
* transaction types are added.
*
* The underlying extraction helpers (`insertNFTokenID`, `insertNFTokenOfferID`)
* live in `NFTokenID.h` and `NFTokenOfferID.h` under the broader `xrpl::`
* namespace so that Clio (the XRPL History API server) can call those helpers
* directly without the `xrpl::RPC` coupling imposed by this header.
*/
#pragma once
#include <xrpl/json/json_forwards.h>
@@ -8,13 +25,44 @@
namespace xrpl::RPC {
/**
Adds common synthetic fields to transaction-related JSON responses
@{
/** Enrich a transaction JSON response with NFT-derived synthetic fields.
*
* Delegates to two independent inserters, in order:
*
* - `insertNFTokenID` — adds `nftoken_id` (for `NFTokenMint` and
* `NFTokenAcceptOffer`) or `nftoken_ids` (for `NFTokenCancelOffer`) by
* diffing the NFToken arrays across all affected ledger nodes recorded in
* the transaction metadata.
* - `insertNFTokenOfferID` — adds `offer_id` for `NFTokenCreateOffer` (and
* mints that include an immediate sell offer) by locating the newly created
* `NFTokenOffer` node and extracting its `sfLedgerIndex`.
*
* Both delegates gate themselves on transaction type and `tesSUCCESS`, so
* this function is safe to call for any transaction type: non-NFT
* transactions produce no output.
*
* Synthetic fields are written into `response[jss::meta]`. The `meta`
* sub-object should already be populated by the caller (e.g., via
* `TxMeta::getJson`) before this function is invoked — consistent with the
* call-site pattern in `Tx.cpp`, `Simulate.cpp`, `AccountTx.cpp`, and
* `NetworkOPs.cpp`, where this call appears alongside `insertDeliveredAmount`
* and `insertMPTokenIssuanceID` as part of a fixed metadata-enrichment
* sequence.
*
* @param response Top-level RPC response object. Synthetic fields are
* written into its `meta` sub-object, which is created on demand if
* absent.
* @param transaction The executed transaction. A null pointer is handled
* gracefully by the delegates (no-op).
* @param transactionMeta Read-only view of the transaction's metadata used to
* diff ledger node states and locate newly created objects.
*
* @see xrpl::insertNFTokenID, xrpl::insertNFTokenOfferID
*/
void
insertNFTSyntheticInJson(json::Value&, std::shared_ptr<STTx const> const&, TxMeta const&);
/** @} */
insertNFTSyntheticInJson(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
} // namespace xrpl::RPC

View File

@@ -1,3 +1,16 @@
/** @file
* Helpers that reconstruct NFToken identities from transaction metadata
* and inject them into RPC JSON responses as synthetic fields.
*
* Raw ledger metadata records before/after state of `NFTokenPage` objects
* but does not directly annotate which token was created or consumed. The
* functions below bridge that gap. They are free (non-static) functions so
* that Clio (the XRPL History API server) can link against them directly
* and perform the same enrichment without duplicating the logic.
*
* @see NFTokenOfferID.h for the analogous helpers for `NFTokenOffer` IDs.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -11,28 +24,100 @@
namespace xrpl {
/**
Add a `nftoken_ids` field to the `meta` output parameter.
The field is only added to successful NFTokenMint, NFTokenAcceptOffer,
and NFTokenCancelOffer transactions.
Helper functions are not static because they can be used by Clio.
@{
/** Returns true if this transaction could have produced or consumed an NFToken.
*
* Acts as a cheap early-exit guard for all downstream extraction logic.
* A transaction qualifies only when it is one of the three NFT types
* (`ttNFTOKEN_MINT`, `ttNFTOKEN_ACCEPT_OFFER`, `ttNFTOKEN_CANCEL_OFFER`)
* and its result code is `tesSUCCESS`. A failed transaction cannot have
* mutated any NFToken page, so metadata diffing would be meaningless.
*
* @param serializedTx The executed transaction; a null pointer yields
* false immediately.
* @param transactionMeta Metadata from the same transaction, used to
* check the result code.
* @return True only when `serializedTx` is non-null, its type is one of
* the three NFT transaction types, and the result is `tesSUCCESS`.
*/
bool
canHaveNFTokenID(std::shared_ptr<STTx const> const& serializedTx, TxMeta const& transactionMeta);
/** Recovers the ID of the NFToken added by a mint transaction.
*
* `ttNFTOKEN_MINT` metadata records the full token arrays of every
* affected `NFTokenPage` in `sfPreviousFields` and `sfFinalFields` but
* does not tag the newly inserted entry. This function recovers it by
* set-difference: it accumulates token IDs from all previous states into
* `prevIDs` and all final states into `finalIDs`, then uses
* `std::mismatch` to locate the first entry present in `finalIDs` but
* absent from `prevIDs`. Because `NFTokenPage` entries are stored in
* sorted order by token ID, both vectors are already ordered and
* `std::mismatch` finds the insertion point in linear time without
* additional sorting.
*
* @note When a mint causes an existing page to split, the linked-list
* rewiring may produce a `sfModifiedNode` for a third page whose
* `sfPreviousFields` contain only pointer updates (`NextPageMin` /
* `PreviousPageMin`) with no `sfNFTokens` array. Such nodes are
* skipped silently; without this guard the size invariant below
* would incorrectly fail for legitimate mints.
*
* @param transactionMeta Metadata from a `ttNFTOKEN_MINT` transaction.
* @return The `uint256` ID of the newly minted token, or `std::nullopt`
* if `finalIDs.size() != prevIDs.size() + 1` (tokens are minted one
* at a time) or if `std::mismatch` unexpectedly reaches the end of
* `finalIDs`.
*/
std::optional<uint256>
getNFTokenIDFromPage(TxMeta const& transactionMeta);
/** Collects the NFToken IDs referenced by deleted `NFTokenOffer` objects.
*
* Both `ttNFTOKEN_ACCEPT_OFFER` and `ttNFTOKEN_CANCEL_OFFER` delete one
* or more `ltNFTOKEN_OFFER` ledger entries. Each deleted offer's
* `sfFinalFields` carries the `sfNFTokenID` it was created for, so the
* token identity is recoverable without set-difference arithmetic.
* Results are sorted and deduplicated because a single cancel transaction
* can target multiple offers that reference the same underlying NFT.
*
* @param transactionMeta Metadata from a `ttNFTOKEN_ACCEPT_OFFER` or
* `ttNFTOKEN_CANCEL_OFFER` transaction.
* @return Sorted, deduplicated vector of `uint256` NFToken IDs recovered
* from all deleted offer nodes; empty if no qualifying deletions are
* found.
*/
std::vector<uint256>
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta);
/** Injects synthetic NFToken ID field(s) into an RPC transaction response.
*
* Calls `canHaveNFTokenID` first; returns immediately without modifying
* `response` if the transaction is ineligible or extraction yields nothing.
* When eligible, dispatches by transaction type:
*
* - `ttNFTOKEN_MINT` — writes `jss::nftoken_id` (single string) derived
* from `getNFTokenIDFromPage`.
* - `ttNFTOKEN_ACCEPT_OFFER` — writes `jss::nftoken_id` (single string,
* first element) derived from `getNFTokenIDFromDeletedOffer`.
* - `ttNFTOKEN_CANCEL_OFFER` — writes `jss::nftoken_ids` (JSON array of
* all deduplicated IDs) derived from `getNFTokenIDFromDeletedOffer`.
*
* The singular/plural field-name distinction reflects a real semantic
* difference: accept and mint affect exactly one NFT, while cancel can
* affect many.
*
* @param response The JSON object to enrich; fields are written
* directly into it. The caller is responsible for scoping this to
* the `jss::meta` sub-object of the full response.
* @param transaction The executed transaction. A null pointer is
* handled gracefully via `canHaveNFTokenID`.
* @param transactionMeta Read-only metadata used for eligibility checking
* and token ID extraction.
*/
void
insertNFTokenID(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace xrpl

View File

@@ -1,3 +1,20 @@
/** @file
* Helpers that recover the ledger index of a newly created `NFTokenOffer`
* from transaction metadata and inject it into RPC JSON responses as a
* synthetic `offer_id` field.
*
* The XRPL transaction format records only inputs; the ledger index of a
* newly created offer object appears solely in the `CreatedNode` entries of
* the transaction metadata. The three functions below encapsulate the scan
* once so that every API consumer — rippled RPC handlers and Clio alike —
* can obtain the offer ID without walking `AffectedNodes` manually. All
* three functions are free (non-static) so that Clio can call them directly
* without duplicating the logic.
*
* @see NFTokenID.h for the analogous helpers that inject `nftoken_id` /
* `nftoken_ids` for mint, accept-offer, and cancel-offer operations.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -10,26 +27,76 @@
namespace xrpl {
/**
Add an `offer_id` field to the `meta` output parameter.
The field is only added to successful NFTokenCreateOffer transactions.
Helper functions are not static because they can be used by Clio.
@{
/** Determine whether a transaction can have an NFToken offer ID.
*
* Acts as a cheap pre-filter before the metadata scan in
* `getOfferIDFromCreatedOffer`. Three conditions must all hold:
*
* 1. `serializedTx` is non-null.
* 2. The transaction type is `ttNFTOKEN_CREATE_OFFER`, **or** it is
* `ttNFTOKEN_MINT` with `sfAmount` present (a mint that simultaneously
* creates an immediate-sale offer).
* 3. The transaction succeeded (`tesSUCCESS`). A failed transaction never
* modifies the ledger, so no offer object can exist in the metadata.
*
* @param serializedTx The transaction to inspect. A null `shared_ptr`
* is handled safely and causes the function to return `false`.
* @param transactionMeta Metadata whose result code is checked for success.
* @return `true` only when all three conditions are satisfied, indicating
* that a subsequent call to `getOfferIDFromCreatedOffer` may yield a
* value.
*/
bool
canHaveNFTokenOfferID(
std::shared_ptr<STTx const> const& serializedTx,
TxMeta const& transactionMeta);
/** Extract the ledger index of the NFToken offer created by a transaction.
*
* Scans the `AffectedNodes` array in `transactionMeta` for a `CreatedNode`
* whose `sfLedgerEntryType` is `ltNFTOKEN_OFFER`. Modified and deleted
* nodes are skipped. The first qualifying node's `sfLedgerIndex` is
* returned; because at most one `NFTokenOffer` can be created per
* transaction, the loop exits immediately on the first match.
*
* @param transactionMeta Read-only transaction metadata to scan.
* @return The `uint256` ledger index of the newly created offer, or
* `std::nullopt` if no `CreatedNode` of type `ltNFTOKEN_OFFER` is
* found. Absence is a plausible non-exceptional condition (e.g., when
* processing historical or externally sourced transactions with
* incomplete metadata), not an error.
*
* @note Callers that have already performed their own eligibility checks
* (e.g., Clio) may call this function directly without first calling
* `canHaveNFTokenOfferID`.
*/
std::optional<uint256>
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta);
/** Inject the NFToken offer ID into a JSON response as `jss::offer_id`.
*
* Composes `canHaveNFTokenOfferID` and `getOfferIDFromCreatedOffer`:
* returns immediately without touching `response` if the transaction is
* ineligible or the metadata contains no created offer node. When an offer
* ID is successfully extracted, it is written into `response[jss::offer_id]`
* as a hex string.
*
* The primary call site is `xrpl::RPC::insertNFTSyntheticInJson`, which
* passes `response[jss::meta]` as the target so that `offer_id` appears
* inside the `meta` sub-object alongside the raw node data.
*
* @param response The JSON object to enrich; `jss::offer_id` is
* written directly into it on success. The caller is responsible for
* scoping this to `jss::meta` of the full response.
* @param transaction The executed transaction. A null pointer is
* handled gracefully (no-op) via `canHaveNFTokenOfferID`.
* @param transactionMeta Read-only transaction metadata used to locate the
* created `NFTokenOffer` node.
*/
void
insertNFTokenOfferID(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace xrpl

View File

@@ -1,3 +1,12 @@
/** @file
* Defines `PathAsset`, the token identifier for a single hop in an XRPL
* payment path, and its associated free functions.
*
* `PathAsset` holds `std::variant<Currency, MPTID>` — just the *which
* currency or MPT* component of a path element, without the issuer.
* This is narrower than `Asset` (`std::variant<Issue, MPTIssue>`) because
* `STPathElement` records the issuer in a separate field.
*/
#pragma once
#include <xrpl/protocol/Asset.h>
@@ -5,7 +14,19 @@
namespace xrpl {
/* Represent STPathElement's asset, which can be Currency or MPTID.
/** Token identifier for a single hop within an XRPL payment path.
*
* Holds `std::variant<Currency, MPTID>` — the *which currency/MPT* component
* of a path element, without the issuer. Issuers are stored separately in
* `STPathElement::mIssuerID` because payment-path serialization records them
* as independent fields; folding them into `PathAsset` would duplicate data
* and complicate encoding.
*
* This is intentionally narrower than `Asset`, which pairs a currency or MPTID
* with its issuer. `PathAsset` carries only the identifier half. Use
* `PathAsset(Asset const&)` to project an `Asset` down to a `PathAsset`.
*
* @see Asset, STPathElement, ValidPathAsset
*/
class PathAsset
{
@@ -14,36 +35,83 @@ private:
public:
PathAsset() = default;
// Enables comparing Asset and PathAsset
/** Construct a PathAsset by projecting an Asset, discarding the issuer.
*
* For an `Issue`-bearing `Asset`, retains the `Currency`. For an
* `MPTIssue`-bearing `Asset`, retains the `MPTID`. This enables direct
* comparison between the richer `Asset` type and the path-element
* representation without manually extracting the identifier.
*
* @param asset The full asset to project.
*/
PathAsset(Asset const& asset);
/** Construct a PathAsset representing an XRP or IOU currency. */
PathAsset(Currency const& currency) : easset_(currency)
{
}
/** Construct a PathAsset representing an MPT issuance. */
PathAsset(MPTID const& mpt) : easset_(mpt)
{
}
/** Return whether the active alternative is exactly `T`.
*
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
* @return `true` if the held alternative is `T`, `false` otherwise.
*/
template <ValidPathAsset T>
[[nodiscard]] constexpr bool
holds() const;
/** Return whether this path asset represents native XRP.
*
* A `Currency` alternative delegates to `xrpl::isXRP(currency)`. An
* `MPTID` alternative always returns `false` — MPT can never be native.
*
* @return `true` if the held currency is the XRP zero-currency sentinel.
*/
[[nodiscard]] constexpr bool
isXRP() const;
/** Return a const reference to the held value of type `T`.
*
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
* @return A reference to the active alternative.
* @throws std::runtime_error if the active alternative is not `T`. Call
* `holds<T>()` or dispatch through `visit()` to avoid this.
*/
template <ValidPathAsset T>
T const&
get() const;
/** Return a const reference to the underlying variant.
*
* Provides direct access to `std::variant<Currency, MPTID>` for callers
* that need to pass it to `std::visit` or store it without going through
* the member `visit()` wrapper.
*
* @return The internal variant holding `Currency` or `MPTID`.
*/
[[nodiscard]] constexpr std::variant<Currency, MPTID> const&
value() const;
// Custom, generic visit implementation
/** Visit the active alternative with a set of per-type callables.
*
* Combines `visitors...` into a single overload set via
* `detail::CombineVisitors` and forwards to `std::visit`. Both
* alternatives (`Currency` and `MPTID`) must be covered.
*
* @tparam Visitors Callable types, one per alternative.
* @param visitors Callables to dispatch to; typically lambdas.
* @return The return value of the selected visitor.
*/
template <typename... Visitors>
constexpr auto
visit(Visitors&&... visitors) const -> decltype(auto)
{
// Simple delegation to the reusable utility, passing the internal
// variant data.
return detail::visit(easset_, std::forward<Visitors>(visitors)...);
}
@@ -51,9 +119,23 @@ public:
operator==(PathAsset const& lhs, PathAsset const& rhs);
};
/** True when `PA` is `Currency`, false when `PA` is `MPTID`.
*
* Compile-time predicate for `if constexpr` branches in generic code that
* must distinguish XRP/IOU paths from MPT paths.
*
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
*/
template <ValidPathAsset PA>
constexpr bool kIS_CURRENCY_V = std::is_same_v<PA, Currency>;
/** True when `PA` is `MPTID`, false when `PA` is `Currency`.
*
* Compile-time predicate for `if constexpr` branches in generic code that
* must distinguish MPT paths from XRP/IOU paths.
*
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
*/
template <ValidPathAsset PA>
constexpr bool kIS_MPTID_V = std::is_same_v<PA, MPTID>;
@@ -94,6 +176,17 @@ PathAsset::isXRP() const
[](MPTID const&) { return false; });
}
/** Compare two PathAssets for equality.
*
* Two `PathAsset` values are equal only when both hold the same alternative
* type *and* the contained values are equal. A `Currency` and an `MPTID`
* are never equal even if their raw bytes coincide, preventing cross-type
* false positives.
*
* @param lhs Left-hand operand.
* @param rhs Right-hand operand.
* @return `true` if both hold the same type and equal value, `false` otherwise.
*/
constexpr bool
operator==(PathAsset const& lhs, PathAsset const& rhs)
{
@@ -112,6 +205,16 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
rhs.value());
}
/** Append a PathAsset's value to a hash state.
*
* Dispatches to the appropriate `hash_append` overload for the active
* alternative (`Currency` or `MPTID`), enabling `PathAsset` to be used as
* a key in hash-based containers built on the `beast::uhash` infrastructure.
*
* @tparam Hasher A type satisfying the `beast::hash_append` Hasher concept.
* @param h The hash accumulator to append to.
* @param pathAsset The path asset whose value is appended.
*/
template <typename Hasher>
void
hash_append(Hasher& h, PathAsset const& pathAsset)
@@ -119,15 +222,41 @@ hash_append(Hasher& h, PathAsset const& pathAsset)
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
}
/** Return whether a PathAsset represents native XRP.
*
* Free-function wrapper for `PathAsset::isXRP()`, provided for symmetry
* with the `isXRP()` overloads for `Currency`, `Asset`, and `STAmount`.
*
* @param asset The path asset to test.
* @return `true` if `asset` holds the XRP zero-currency sentinel.
*/
inline bool
isXRP(PathAsset const& asset)
{
return asset.isXRP();
}
/** Produce a human-readable string identifying a PathAsset.
*
* Dispatches to `to_string(Currency const&)` or `to_string(MPTID const&)`
* depending on the active alternative. For a `Currency` this yields the ISO
* 4217 ticker or `"XRP"`; for an `MPTID` it yields the base-58 encoded token
* identifier.
*
* @param asset The path asset to stringify.
* @return A descriptive string identifying the currency or MPT issuance.
*/
std::string
to_string(PathAsset const& asset);
/** Stream-insert a human-readable description of a PathAsset.
*
* Equivalent to `os << to_string(x)`. Intended for logging and diagnostics.
*
* @param os The output stream to write to.
* @param x The path asset to write.
* @return `os`, for chaining.
*/
std::ostream&
operator<<(std::ostream& os, PathAsset const& x);

View File

@@ -1,3 +1,13 @@
/** @file
* Canonical serialization for payment channel claim authorizations.
*
* Defines the single function that all three call sites — channel
* authorization (RPC), channel verification (RPC), and on-ledger
* claim validation (transaction engine) — must use to build the
* signed payload. Centralizing this here ensures that a signature
* produced off-ledger is always accepted on-ledger.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -7,6 +17,34 @@
namespace xrpl {
/** Serialize the signing payload for a payment channel claim authorization.
*
* Writes exactly three fields into @p msg in a protocol-defined order:
* the `HashPrefix::paymentChannelClaim` domain-separation tag (4 bytes),
* the 256-bit channel keylet @p key, and the authorized cumulative amount
* @p amt as a 64-bit drop count. The resulting byte sequence is what the
* channel sender signs and what the recipient or ledger verifies.
*
* This function is the single source of truth for the signed payload layout.
* It is called identically by `channel_authorize` (RPC), `channel_verify`
* (RPC), and `PaymentChannelClaim` preflight (transaction engine). Any drift
* between those sites would cause off-ledger signatures to fail on-ledger
* validation.
*
* @param msg Serializer to append the payload fields into. The caller is
* responsible for constructing the `Serializer` and, after this call,
* passing `msg.slice()` to the sign or verify primitive.
* @param key The 256-bit keylet of the payment channel ledger object. Binds
* the authorization to exactly one channel so it cannot be replayed
* against a different channel.
* @param amt The authorized cumulative ceiling in drops. The on-ledger claim
* validator rejects any claim whose running total exceeds this value.
*
* @note The `HashPrefix::paymentChannelClaim` tag (`'C','L','M',0x00`) is
* protocol-immutable. Changing it would invalidate all existing payment
* channel authorizations.
* @see HashPrefix::paymentChannelClaim
*/
inline void
serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt)
{

View File

@@ -1,3 +1,17 @@
/** @file
* Central definition of XRPL's account-delegation permission system,
* used by the `DelegateSet` transaction type.
*
* Two numeric ranges partition the `sfPermissionValue` field stored
* on-ledger:
* - **Transaction-level** (≤ `UINT16_MAX`): `TxType + 1`, granting
* authority over an entire transaction type.
* - **Granular** (> `UINT16_MAX`, minimum 65537): covers a specific
* sub-operation within a transaction type (e.g., freezing a trustline
* without being able to authorize it).
*
* The `Permission` singleton is the runtime authority for both ranges.
*/
#pragma once
#include <xrpl/protocol/Rules.h>
@@ -9,12 +23,21 @@
#include <unordered_map>
namespace xrpl {
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
* Permissions, only the GranularPermissionType is defined here. To prevent
* conflicts with TxType, the GranularPermissionType is always set to a value
* greater than the maximum value of uint16.
/** Granular sub-operation permission values used by the delegation system.
*
* Each enumerator targets a specific capability within a parent transaction
* type, enabling fine-grained delegation without granting broad transaction-
* level authority. For example, `TrustlineFreeze` delegates only the ability
* to freeze a trustline via `ttTRUST_SET`, not to authorize or unfreeze.
*
* All values are greater than `UINT16_MAX` (minimum 65537), which keeps them
* numerically disjoint from transaction-level permissions (≤ `UINT16_MAX`).
* This invariant is asserted at startup inside the `Permission` constructor.
*
* Generated from `detail/permissions.macro` via the X-macro pattern. Adding
* a new sub-operation requires only a single `PERMISSION(...)` entry in that
* file.
*/
// Macro-generated, complex
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
@@ -30,27 +53,67 @@ enum GranularPermissionType : std::uint32_t {
#pragma pop_macro("PERMISSION")
};
/** Indicates whether a transaction type may be delegated in bulk via
* a transaction-level `DelegateSet` permission.
*
* The policy for each `TxType` is encoded in `detail/transactions.macro`
* as the `delegable` parameter of every `TRANSACTION(...)` entry.
* Sensitive types such as `ttACCOUNT_SET` and `ttREGULAR_KEY_SET` are
* `NotDelegable`; most operational types are `Delegable`.
*
* @note Bare enumerators (`xrpl::Delegable` / `xrpl::NotDelegable`) are
* required by preprocessor expansions in tests and macro-generated
* code; `enum class` would break that usage.
*/
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
// tricks in tests and macro-generated code; enum class would break that.
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Delegation { Delegable, NotDelegable };
/** Central authority for XRPL's account-delegation permission system.
*
* A Meyer's singleton populated at first call to `getInstance()`. Its
* constructor expands `transactions.macro` and `permissions.macro` to build
* five immutable lookup maps covering every known transaction type and
* granular sub-operation. After construction the maps are never mutated,
* so all concurrent read access from transaction-processing threads is safe
* without synchronization.
*
* The two principal call sites are:
* - `DelegateSet::preflight()` — calls `isDelegable()` to validate each
* `sfPermissionValue` before it is written on-ledger.
* - `DelegateUtils` / transactors — call `getGranularTxType()` and related
* helpers to enforce granular limits at execution time.
*/
class Permission
{
private:
Permission();
/** Maps each `TxType` to the amendment required to use it, or `uint256{}` if none. */
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
/** Maps each `TxType` to its `Delegable` / `NotDelegable` policy tag. */
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
/** Maps granular permission name strings to their `GranularPermissionType` values. */
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
/** Maps `GranularPermissionType` values to their name strings (inverse of `granularPermissionMap_`). */
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
/** Maps each `GranularPermissionType` to its parent `TxType`. */
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
public:
/** Returns the process-wide singleton instance.
*
* Initialized on first call via a function-local `static`; C++11
* guarantees thread-safe initialization. The instance is never mutated
* after construction.
*
* @return A `const` reference to the singleton `Permission` object.
*/
static Permission const&
getInstance();
@@ -58,29 +121,125 @@ public:
Permission&
operator=(Permission const&) = delete;
/** Resolves a raw `sfPermissionValue` to its human-readable name.
*
* Checks the granular permission table first (values > `UINT16_MAX`).
* If unrecognized there, decodes the value as a transaction-level
* permission (`value - 1` = `TxType`) and delegates to `TxFormats` for
* the canonical name. Used by `STUInt32::getText()` and
* `STUInt32::getJson()` to render any `sfPermissionValue` as a string
* instead of a raw number.
*
* @param value Raw `sfPermissionValue` from the ledger.
* @return The permission name, or `std::nullopt` if `value` is not
* recognized as either a granular or transaction-level permission.
*/
[[nodiscard]] std::optional<std::string>
getPermissionName(std::uint32_t const value) const;
/** Looks up the numeric wire value of a granular permission by name.
*
* Used when deserializing `sfPermissionValue` from JSON (e.g., during
* `DelegateSet` preflight or RPC input parsing) to convert a
* human-readable name like `"TrustlineFreeze"` back to its `uint32_t`
* representation.
*
* @param name Case-sensitive granular permission name.
* @return The corresponding `uint32_t` wire value, or `std::nullopt` if
* `name` is not a known granular permission.
*/
[[nodiscard]] std::optional<std::uint32_t>
getGranularValue(std::string const& name) const;
/** Looks up the name of a granular permission by its enum value.
*
* Inverse of `getGranularValue`; used when serializing a granular
* permission value to human-readable output.
*
* @param value A `GranularPermissionType` enum value.
* @return The permission name string, or `std::nullopt` if `value` is
* not a known granular permission.
*/
[[nodiscard]] std::optional<std::string>
getGranularName(GranularPermissionType const& value) const;
/** Returns the parent transaction type for a granular permission.
*
* Multiple granular permissions share the same parent `TxType`; for
* example, `TrustlineAuthorize`, `TrustlineFreeze`, and
* `TrustlineUnfreeze` all map to `ttTRUST_SET`. Used by `isDelegable()`
* and execution-time helpers to locate the relevant transactor context
* and required amendment for a granular sub-operation.
*
* @param gpType A `GranularPermissionType` enum value.
* @return The parent `TxType`, or `std::nullopt` if `gpType` is not a
* known granular permission.
*/
[[nodiscard]] std::optional<TxType>
getGranularTxType(GranularPermissionType const& gpType) const;
/** Returns the amendment required to use a transaction type, if any.
*
* A `uint256{}` stored in `txFeatureMap_` means the transaction type
* requires no enabling amendment. In that case `std::nullopt` is
* returned, signalling that the type is unconditionally available.
*
* @param txType A recognized transaction type.
* @return A const reference to the required amendment hash wrapped in
* `std::optional`, or `std::nullopt` if no amendment is required.
* @note Asserts in debug builds that `txType` is present in
* `txFeatureMap_`. Passing an unregistered `TxType` is a
* programming error (a transaction missing from `transactions.macro`).
*/
[[nodiscard]] std::optional<std::reference_wrapper<uint256 const>>
getTxFeature(TxType txType) const;
/** Determines whether a permission value may appear in a `DelegateSet`
* transaction under the current ledger rules.
*
* The check differs by permission kind:
* - **Granular** (value > `UINT16_MAX`): accepted whenever the value
* resolves to a known `GranularPermissionType`; no further gate is
* applied because granular permissions are inherently narrow.
* - **Transaction-level** (value ≤ `UINT16_MAX`): accepted only when the
* decoded `TxType` is recognized, its required amendment is currently
* enabled in `rules` (or no amendment is required), and the type is
* marked `Delegable` in `transactions.macro`.
*
* @param permissionValue Raw `sfPermissionValue` to validate.
* @param rules Active amendment rules for the current ledger.
* @return `true` if the permission may be granted, `false` otherwise.
* @note The amendment check prevents a transaction type from being
* delegated before the ledger feature that introduces it is live,
* even if the macro table already includes it.
*/
[[nodiscard]] bool
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
// for tx level permission, permission value is equal to tx type plus one
/** Converts a `TxType` to its transaction-level permission value.
*
* Transaction-level permissions are encoded as `TxType + 1`. The `+1`
* offset ensures zero is never a valid permission value and keeps the
* entire range within `uint16` (transaction-level permissions ≤
* `UINT16_MAX`).
*
* @param type A transaction type.
* @return The corresponding `uint32_t` permission value (`TxType + 1`).
* @see permissionToTxType
*/
static uint32_t
txToPermissionType(TxType const& type);
// tx type value is permission value minus one
/** Converts a transaction-level permission value back to its `TxType`.
*
* Inverse of `txToPermissionType`. Callers must verify that `value` is
* in the transaction-level range (≤ `UINT16_MAX`) before calling; this
* function performs no range check.
*
* @param value A transaction-level permission value (`TxType + 1`).
* @return The decoded `TxType` (`value - 1`).
* @see txToPermissionType
*/
static TxType
permissionToTxType(uint32_t const& value);
};

View File

@@ -1,3 +1,16 @@
/** @file
* Canonical source of XRPL protocol constants and boundary predicates.
*
* Every hard-coded numeric limit that, if changed silently, would cause a
* **hard fork** — a ledger-state disagreement between nodes running different
* software versions — is defined here. All constants are `constexpr` and
* therefore available at compile time with zero runtime overhead.
*
* @note Changing any value in this file without pairing the change with an
* amendment-gated detection mechanism will split the network.
*
* @ingroup protocol
*/
#pragma once
#include <xrpl/basics/ByteUtilities.h>
@@ -8,100 +21,182 @@
namespace xrpl {
/** Protocol specific constants.
This information is, implicitly, part of the protocol.
@note Changing these values without adding code to the
server to detect "pre-change" and "post-change"
will result in a hard fork.
@ingroup protocol
*/
/** Smallest legal byte size of a transaction. */
/** Smallest legal serialized size of a transaction, in bytes.
*
* Transactions below this threshold are trivially malformed and are rejected
* before deserialization begins.
*/
std::size_t constexpr kTX_MIN_SIZE_BYTES = 32;
/** Largest legal byte size of a transaction. */
/** Largest legal serialized size of a transaction, in bytes.
*
* The 1 MB cap protects node memory and network bandwidth. Transactions
* exceeding this limit are rejected on receipt without further processing.
*/
std::size_t constexpr kTX_MAX_SIZE_BYTES = megabytes(1);
/** The maximum number of unfunded offers to delete at once */
/** Maximum number of unfunded offers that may be removed in a single
* transaction pass.
*
* Unfunded-offer cleanup is opportunistic: stale offers are removed as a
* side-effect of offer placement. Capping the count keeps the worst-case
* execution time of a single transaction predictable.
*
* @note The asymmetry with `kEXPIRED_OFFER_REMOVE_LIMIT` (1000 vs 256)
* reflects that unfunded-offer removal was designed to handle larger
* batches; expired offers are discovered through a different, narrower
* path.
*/
std::size_t constexpr kUNFUNDED_OFFER_REMOVE_LIMIT = 1000;
/** The maximum number of expired offers to delete at once */
/** Maximum number of expired offers that may be removed in a single
* transaction pass.
*
* @see kUNFUNDED_OFFER_REMOVE_LIMIT for the rationale behind the asymmetric
* cap.
*/
std::size_t constexpr kEXPIRED_OFFER_REMOVE_LIMIT = 256;
/** The maximum number of metadata entries allowed in one transaction */
/** Maximum number of metadata entries a single transaction may produce.
*
* When a transaction would exceed this cap the transactor returns
* `tecOVERSIZE`, triggering a controlled teardown that applies the fee
* and rolls back ledger mutations rather than allowing unbounded metadata
* growth.
*/
std::size_t constexpr kOVERSIZE_META_DATA_CAP = 5200;
/** The maximum number of entries per directory page */
/** Maximum number of entries per owner-directory or offer-directory page.
*
* Keeping pages small bounds the work required to traverse a directory:
* each page hop visits at most 32 entries.
*/
std::size_t constexpr kDIR_NODE_MAX_ENTRIES = 32;
/** The maximum number of pages allowed in a directory
Made obsolete by fixDirectoryLimit amendment.
*/
/** Historical maximum number of pages in a single directory.
*
* This limit was enforced before the `fixDirectoryLimit` amendment.
* Post-amendment, directories may grow beyond 262 144 pages; this
* constant is retained for pre-amendment replay correctness.
*
* @note Pre-amendment code returns `tecDIR_FULL` when this limit is
* reached. Post-amendment, only unsigned-integer overflow can
* produce a null page index.
*/
std::uint64_t constexpr kDIR_NODE_MAX_PAGES = 262144;
/** The maximum number of items in an NFT page */
/** Maximum number of NFToken entries per NFT directory page. */
std::size_t constexpr kDIR_MAX_TOKENS_PER_PAGE = 32;
/** The maximum number of owner directory entries for account to be deletable */
/** Maximum number of owner-directory entries an account may hold and still
* be eligible for deletion via `AccountDelete`.
*
* Accounts with more than 1000 directory entries cannot be deleted; this
* protects against unbounded cleanup work within a single transaction.
*/
std::size_t constexpr kMAX_DELETABLE_DIR_ENTRIES = 1000;
/** The maximum number of token offers that can be canceled at once */
/** Maximum number of NFToken offers that may be cancelled in a single
* `NFTokenCancelOffer` transaction.
*/
std::size_t constexpr kMAX_TOKEN_OFFER_CANCEL_COUNT = 500;
/** The maximum number of offers in an offer directory for NFT to be burnable */
/** Maximum number of NFToken offers that must be cleaned up before an NFT
* can be burned.
*
* An NFT with more than 500 live offers cannot be burned until the excess
* offers are cancelled first.
*/
std::size_t constexpr kMAX_DELETABLE_TOKEN_OFFER_ENTRIES = 500;
/** The maximum token transfer fee allowed.
Token transfer fees can range from 0% to 50% and are specified in tenths of
a basis point; that is a value of 1000 represents a transfer fee of 1% and
a value of 10000 represents a transfer fee of 10%.
Note that for extremely low transfer fees values, it is possible that the
calculated fee will be 0.
/** Maximum NFToken transfer fee, expressed in tenths of a basis point.
*
* Transfer fees range from 0% to 50%. A value of 1 000 represents 1% and
* a value of 50 000 represents 50%. For very low fee values the computed
* fee amount may round down to zero drops.
*/
std::uint16_t constexpr kMAX_TRANSFER_FEE = 50000;
/** There are 10,000 basis points (bips) in 100%.
/** Number of basis points (bips) in 100% (unity).
*
* Basis points represent 0.01%.
* One basis point equals 0.01%. To compute the share of a value `X`
* corresponding to `B` bips, use `X * B / kBIPS_PER_UNITY`. To convert
* a whole-percentage `P` to bips, use `P * kBIPS_PER_UNITY / 100`
* (or simply call `percentageToBips(P)`).
*
* Given a value X, to find the amount for B bps,
* use X * B / bipsPerUnity
* Example: 10% coverage on 999 XRP (999 000 000 drops) =
* `999'000'000 * 1'000 / 10'000` = 99 900 000 drops.
*
* Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of
* that debt as cover (10%), then the minimum cover amount is 999,000,000 drops
* * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP.
*
* Given a percentage P, to find the number of bps that percentage represents,
* use P * bipsPerUnity.
*
* Example: 50% is 0.50 * bipsPerUnity = 5,000 bps.
* All ledger fee and rate arithmetic uses integer bips to guarantee
* bit-identical results across all validator platforms.
*/
Bips32 constexpr kBIPS_PER_UNITY(100 * 100);
static_assert(kBIPS_PER_UNITY == Bips32{10'000});
/** Number of tenth-basis-points in 100% (unity).
*
* One tenth-basis-point equals 0.001%. Use `percentageToTenthBips(P)`
* to convert a whole percentage, or `tenthBipsOfValue(value, rate)` to
* apply a rate to a value.
*/
TenthBips32 constexpr kTENTH_BIPS_PER_UNITY(kBIPS_PER_UNITY.value() * 10);
static_assert(kTENTH_BIPS_PER_UNITY == TenthBips32(100'000));
/** Convert a whole-percentage value to a strongly-typed `Bips32`.
*
* Uses integer division; fractional basis points are truncated.
*
* @param percentage An integer percentage in [0, 100].
* @return The equivalent number of basis points as a `Bips32`.
*/
constexpr Bips32
percentageToBips(std::uint32_t percentage)
{
return Bips32(percentage * kBIPS_PER_UNITY.value() / 100);
}
/** Convert a whole-percentage value to a strongly-typed `TenthBips32`.
*
* Uses integer division; fractional tenth-bips are truncated.
*
* @param percentage An integer percentage in [0, 100].
* @return The equivalent number of tenth-basis-points as a `TenthBips32`.
*/
constexpr TenthBips32
percentageToTenthBips(std::uint32_t percentage)
{
return TenthBips32(percentage * kTENTH_BIPS_PER_UNITY.value() / 100);
}
/** Compute the basis-point share of a value using integer arithmetic.
*
* Calculates `value * bips / kBIPS_PER_UNITY` without floating point,
* guaranteeing deterministic results on all platforms.
*
* @tparam T Numeric type of the value (must support `*` and `/`).
* @tparam TBips Underlying storage type of the `Bips` wrapper.
* @param value The base amount to take a share of.
* @param bips The rate in basis points.
* @return The share of `value` at the given rate, truncated toward zero.
*/
template <typename T, class TBips>
constexpr T
bipsOfValue(T value, Bips<TBips> bips)
{
return value * bips.value() / kBIPS_PER_UNITY.value();
}
/** Compute the tenth-basis-point share of a value using integer arithmetic.
*
* Calculates `value * bips / kTENTH_BIPS_PER_UNITY` without floating
* point, guaranteeing deterministic results on all platforms.
*
* @tparam T Numeric type of the value (must support `*` and `/`).
* @tparam TBips Underlying storage type of the `TenthBips` wrapper.
* @param value The base amount to take a share of.
* @param bips The rate in tenth-basis-points.
* @return The share of `value` at the given rate, truncated toward zero.
*/
template <typename T, class TBips>
constexpr T
tenthBipsOfValue(T value, TenthBips<TBips> bips)
@@ -109,202 +204,293 @@ tenthBipsOfValue(T value, TenthBips<TBips> bips)
return value * bips.value() / kTENTH_BIPS_PER_UNITY.value();
}
/** Rate and limit constants specific to the on-ledger lending protocol. */
namespace Lending {
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
Valid values are between 0 and 10% inclusive.
*/
/** Maximum management fee a LoanBroker may charge, in tenth-basis-points.
*
* Valid values are in [0, 10%]. Stored as `TenthBips16` (fits in
* `uint16_t`) because 10 000 < 65 535.
*/
TenthBips16 constexpr kMAX_MANAGEMENT_FEE_RATE(
unsafeCast<std::uint16_t>(percentageToTenthBips(10).value()));
static_assert(kMAX_MANAGEMENT_FEE_RATE == TenthBips16(std::uint16_t(10'000u)));
/** The maximum coverage rate required of a loan broker in 1/10 bips.
Valid values are between 0 and 100% inclusive.
*/
/** Maximum coverage rate a LoanBroker must maintain, in tenth-basis-points.
*
* The coverage rate specifies the minimum fraction of outstanding loan
* debt that the broker must hold as collateral. Valid values are in
* [0, 100%].
*/
TenthBips32 constexpr kMAX_COVER_RATE = percentageToTenthBips(100);
static_assert(kMAX_COVER_RATE == TenthBips32(100'000u));
/** The maximum overpayment fee on a loan in 1/10 bips.
*
Valid values are between 0 and 100% inclusive.
*/
/** Maximum overpayment fee on a loan, in tenth-basis-points.
*
* Applied when a borrower pays more than the scheduled amount. Valid
* values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_OVERPAYMENT_FEE = percentageToTenthBips(100);
static_assert(kMAX_OVERPAYMENT_FEE == TenthBips32(100'000u));
/** Annualized interest rate of the Loan in 1/10 bips.
/** Maximum annualized interest rate on a Loan, in tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum premium added to the interest rate for late payments on a loan
* in 1/10 bips.
/** Maximum late-payment interest premium on a Loan, in tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* This rate is added to the base interest rate when payments are overdue.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_LATE_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_LATE_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum close interest rate charged for repaying a loan early in 1/10
* bips.
/** Maximum early-repayment (close) interest rate on a Loan, in
* tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Charged when a borrower repays a loan ahead of schedule. Valid values
* are in [0, 100%].
*/
TenthBips32 constexpr kMAX_CLOSE_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_CLOSE_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum overpayment interest rate charged on loan overpayments in 1/10
* bips.
/** Maximum overpayment interest rate charged on loan overpayments, in
* tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_OVERPAYMENT_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_OVERPAYMENT_INTEREST_RATE == TenthBips32(100'000u));
/** LoanPay transaction cost will be one base fee per X combined payments
/** Number of loan payments per base-fee increment charged by `LoanPay`.
*
* The number of payments is estimated based on the Amount paid and the Loan's
* Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag)
* count as one more payment.
* The fee is estimated from the transaction `Amount` divided by the
* loan's fixed payment size. Overpayments (flagged with
* `tfLoanOverpayment`) count as one additional payment in the estimate.
* One base fee unit is charged for every 5 estimated payments.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
* @note This value was chosen arbitrarily and is amendment-locked once
* released: changing it without an amendment would alter the fee
* schedule for existing `LoanPay` transactions.
* @see kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION
*/
static constexpr int kLOAN_PAYMENTS_PER_FEE_INCREMENT = 5;
/** Maximum number of combined payments that a LoanPay transaction will process
/** Hard cap on the number of combined payments processed by one `LoanPay`.
*
* This limit is enforced during the loan payment process, and thus is not
* estimated. If the limit is hit, no further payments or overpayments will be
* processed, no matter how much of the transaction Amount is left, but the
* transaction will succeed with the payments that have been processed up to
* that point.
* This limit is enforced during execution, not during fee estimation.
* When the cap is reached the transaction succeeds with the payments
* processed so far; any remaining `Amount` is not applied.
*
* This limit is independent of loanPaymentsPerFeeIncrement, so a transaction
* could potentially be charged for many more payments than actually get
* processed. Users should take care not to submit a transaction paying more
* than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because
* overpayments are charged as a payment, if submitting
* loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not
* set the tfLoanOverpayment flag.
* Because the fee is based on the *estimated* payment count (derived from
* `Amount / PeriodicPayment`) and the cap is enforced on the *actual*
* count, a transaction can be charged for more payments than it processes.
* Submitters should not exceed
* `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION * Loan.PeriodicPayment` in
* `Amount`, and should omit `tfLoanOverpayment` if paying exactly that
* much.
*
* Even though they're independent, loanMaximumPaymentsPerTransaction should be
* a multiple of loanPaymentsPerFeeIncrement.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
* @note `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION` must remain a multiple
* of `kLOAN_PAYMENTS_PER_FEE_INCREMENT`; this invariant is checked
* at startup via `static_assert` in LoanPay.cpp. Both values are
* amendment-locked once released.
*/
static constexpr int kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION = 100;
} // namespace Lending
/** The maximum length of a URI inside an NFT */
/** Maximum byte length of a URI stored in an NFToken. */
std::size_t constexpr kMAX_TOKEN_URI_LENGTH = 256;
/** The maximum length of a Data element inside a DID */
/** Maximum byte length of the `Data` field (DID document) in a DID object. */
std::size_t constexpr kMAX_DID_DOCUMENT_LENGTH = 256;
/** The maximum length of a URI inside a DID */
/** Maximum byte length of the `URI` field in a DID object. */
std::size_t constexpr kMAX_DIDURI_LENGTH = 256;
/** The maximum length of an Attestation inside a DID */
/** Maximum byte length of the `Attestation` field in a DID object. */
std::size_t constexpr kMAX_DID_DATA_LENGTH = 256;
/** The maximum length of a domain */
/** Maximum byte length of an account `Domain` field. */
std::size_t constexpr kMAX_DOMAIN_LENGTH = 256;
/** The maximum length of a URI inside a Credential */
/** Maximum byte length of the `URI` field in a Credential object. */
std::size_t constexpr kMAX_CREDENTIAL_URI_LENGTH = 256;
/** The maximum length of a CredentialType inside a Credential */
/** Maximum byte length of the `CredentialType` field in a Credential object.
*
* Narrower than the 256-byte default to keep credential-type strings
* human-readable and prevent abuse of the type field as an arbitrary blob.
*/
std::size_t constexpr kMAX_CREDENTIAL_TYPE_LENGTH = 64;
/** The maximum number of credentials can be passed in array */
/** Maximum number of credentials that may appear in a transaction's
* `Credentials` array.
*/
std::size_t constexpr kMAX_CREDENTIALS_ARRAY_SIZE = 8;
/** The maximum number of credentials can be passed in array for permissioned
* domain */
/** Maximum number of credentials that a permissioned domain may reference. */
std::size_t constexpr kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE = 10;
/** The maximum length of MPTokenMetadata */
/** Maximum byte length of the `MPTokenMetadata` field on an MPTokenIssuance. */
std::size_t constexpr kMAX_MP_TOKEN_METADATA_LENGTH = 1024;
/** The maximum amount of MPTokenIssuance */
/** Maximum quantity representable by an MPToken amount field.
*
* Equal to `INT64_MAX` (2^63 1). The `static_assert` below guarantees
* that the XRPL `Number` type can represent every valid MPToken quantity
* without overflow.
*/
std::uint64_t constexpr kMAX_MP_TOKEN_AMOUNT = 0x7FFF'FFFF'FFFF'FFFFull;
static_assert(Number::kMAX_REP >= kMAX_MP_TOKEN_AMOUNT);
/** The maximum length of Data payload */
/** Maximum byte length of the `Data` payload field. */
std::size_t constexpr kMAX_DATA_PAYLOAD_LENGTH = 256;
/** Vault withdrawal policies */
/** Vault withdrawal policy: first-come, first-served.
*
* The numeric value 1 is the wire-stable identifier for this strategy;
* it must not change once released.
*/
std::uint8_t constexpr kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE = 1;
/** Default IOU scale factor for a Vault */
/** Default IOU-to-share scale exponent for a Vault.
*
* When no explicit scale is specified at Vault creation the scale
* defaults to 6, meaning one IOU unit maps to 10^6 shares. This
* applies only to IOU-backed vaults; native-asset and MPT vaults always
* use scale 0.
*/
std::uint8_t constexpr kVAULT_DEFAULT_IOU_SCALE = 6;
/** Maximum scale factor for a Vault. The number is chosen to ensure that
1 IOU can be always converted to shares.
10^19 > maxMPTokenAmount (2^64-1) > 10^18 */
/** Maximum IOU-to-share scale exponent for a Vault.
*
* Chosen so that exactly one IOU unit can always be converted to at
* least one share: 10^19 > `kMAX_MP_TOKEN_AMOUNT` (≈ 2^63) > 10^18.
* Preflight rejects any `VaultCreate` that specifies a scale above this
* value with `temMALFORMED`. Applies only to IOU-backed vaults.
*/
std::uint8_t constexpr kVAULT_MAXIMUM_IOU_SCALE = 18;
/** Maximum recursion depth for vault shares being put as an asset inside
* another vault; counted from 0 */
/** Maximum recursion depth when checking whether a vault's asset is itself
* backed by another vault.
*
* Counted from 0, so a depth of 5 permits at most 6 levels of nesting.
* This prevents pathological chains from consuming unbounded stack space
* during asset-validation traversal.
*/
std::uint8_t constexpr kMAX_ASSET_CHECK_DEPTH = 5;
/** A ledger index. */
/** Ledger sequence number type.
*
* A named alias for `uint32_t` that makes function signatures
* self-documenting wherever ledger positions are passed.
*/
using LedgerIndex = std::uint32_t;
/** Number of ledgers between consecutive flag-ledger boundaries.
*
* Every 256 ledgers the network applies accumulated validator votes for
* fee adjustments, reserve requirements, amendment activation, and
* Negative UNL reliability scoring. Both `isFlagLedger()` and
* `isVotingLedger()` test `seq % kFLAG_LEDGER_INTERVAL == 0`; the
* semantic distinction between the two predicates is resolved by callers
* via a `+1` offset on the sequence number they pass.
*
* @note This constant is an implicit part of the wire protocol. Changing
* it without an amendment-gated migration path will cause a hard fork.
*/
std::uint32_t constexpr kFLAG_LEDGER_INTERVAL = 256;
/** Returns true if the given ledgerIndex is a voting ledgerIndex */
/** Return `true` if @p seq is a voting ledger.
*
* Semantically, this asks: "will the ledger built *on top of* `seq`
* be a flag ledger?" Callers therefore pass `seq + 1` (the sequence of
* the ledger currently being assembled). `RCLConsensus` uses this
* predicate to decide whether to inject Negative UNL pseudo-transactions
* for the new consensus round.
*
* The arithmetic is identical to `isFlagLedger`; the two names exist to
* make the `+1` offset explicit at each call site without embedding it
* inside these functions.
*
* @param seq The ledger index to test (typically the previous ledger's
* sequence plus one).
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
* @see isFlagLedger
*/
bool
isVotingLedger(LedgerIndex seq);
/** Returns true if the given ledgerIndex is a flag ledgerIndex */
/** Return `true` if @p seq is a flag ledger.
*
* A flag ledger is any ledger whose sequence number is an exact multiple
* of `kFLAG_LEDGER_INTERVAL` (256). It is the ledger in which fee-vote
* and amendment pseudo-transactions are applied, and in which Negative
* UNL reliability updates take effect. `Change::doApply` and
* `FeeVoteImpl` gate their parameter-update logic on this predicate.
*
* Callers pass the ledger's **own** sequence number to ask "has this
* ledger already crossed the boundary?", as opposed to `isVotingLedger`,
* which is called with `seq + 1`.
*
* @param seq The ledger index to test.
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
* @see isVotingLedger
*/
bool
isFlagLedger(LedgerIndex seq);
/** A transaction identifier.
The value is computed as the hash of the
canonicalized, serialized transaction object.
*/
/** Transaction identifier type.
*
* A 256-bit hash computed over the canonicalized, serialized transaction
* object using `HashPrefix::transactionID` as the domain separator.
*/
using TxID = uint256;
/** The maximum number of trustlines to delete as part of AMM account
* deletion cleanup.
/** Maximum number of AMM trust lines that may be deleted as part of an
* AMM account-deletion cleanup pass.
*/
std::uint16_t constexpr kMAX_DELETABLE_AMM_TRUST_LINES = 512;
/** The maximum length of a URI inside an Oracle */
/** Maximum byte length of the `URI` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_URI = 256;
/** The maximum length of a Provider inside an Oracle */
/** Maximum byte length of the `Provider` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_PROVIDER = 256;
/** The maximum size of a data series array inside an Oracle */
/** Maximum number of price data-series entries in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_DATA_SERIES = 10;
/** The maximum length of a SymbolClass inside an Oracle */
/** Maximum byte length of the `SymbolClass` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_SYMBOL_CLASS = 16;
/** The maximum allowed time difference between lastUpdateTime and the time
of the last closed ledger
*/
/** Maximum allowed age of an Oracle price update, in seconds.
*
* `OracleSet` rejects updates whose `LastUpdateTime` differs from the
* last-closed-ledger close time by more than 300 seconds (5 minutes).
*/
std::size_t constexpr kMAX_LAST_UPDATE_TIME_DELTA = 300;
/** The maximum price scaling factor
*/
/** Maximum price-scaling exponent accepted in an Oracle object. */
std::size_t constexpr kMAX_PRICE_SCALE = 20;
/** The maximum percentage of outliers to trim
/** Maximum percentage of outlier data points to trim in Oracle price
* aggregation.
*/
std::size_t constexpr kMAX_TRIM = 25;
/** The maximum number of delegate permissions an account can grant
*/
/** Maximum number of granular delegate permissions an account may grant. */
std::size_t constexpr kPERMISSION_MAX_SIZE = 10;
/** The maximum number of transactions that can be in a batch. */
/** Maximum number of inner transactions in a single Batch transaction.
*
* Enforced during preflight; batches exceeding this count are rejected.
* The limit directly bounds the worst-case compute cost for batch
* signature validation and fee calculation.
*/
std::size_t constexpr kMAX_BATCH_TX_COUNT = 8;
} // namespace xrpl

View File

@@ -16,35 +16,31 @@
namespace xrpl {
/** A public key.
Public keys are used in the public-key cryptography
system used to verify signatures attached to messages.
The format of the public key is XRPL specific,
information needed to determine the cryptosystem
parameters used is stored inside the key.
As of this writing two systems are supported:
secp256k1
ed25519
secp256k1 public keys consist of a 33 byte
compressed public key, with the lead byte equal
to 0x02 or 0x03.
The ed25519 public keys consist of a 1 byte
prefix constant 0xED, followed by 32 bytes of
public key data.
*/
/** Immutable 33-byte value type holding an XRPL public key.
*
* Supports both secp256k1 and Ed25519 cryptosystems. The lead byte acts as
* a self-describing type tag — `0x02`/`0x03` for secp256k1 compressed keys,
* `0xED` for Ed25519 keys (an XRPL-specific prefix that pads the native
* 32-byte Ed25519 key to the common 33-byte size). This uniform encoding
* allows `publicKeyType()` to identify the algorithm in O(1) from the raw
* bytes alone, with no external metadata.
*
* The default constructor is deleted; the only construction path is from a
* `Slice`. If the slice does not represent a recognized key format,
* construction calls `LogicError` (process termination) rather than
* throwing — an invalid key at this point indicates a programming error,
* not a recoverable runtime condition. Any live `PublicKey` object is
* therefore always well-formed and algorithm-identified.
*
* The implicit conversion to `Slice` is intentional: it lets `PublicKey`
* flow into serialization and hashing APIs without explicit casting.
*/
class PublicKey
{
protected:
// All the constructed public keys are valid, non-empty and contain 33
// bytes of data.
/** Uniform storage size in bytes for all supported key types. */
static constexpr std::size_t kSIZE = 33;
std::uint8_t buf_[kSIZE]{}; // should be large enough
std::uint8_t buf_[kSIZE]{};
public:
using const_iterator = std::uint8_t const*;
@@ -56,72 +52,89 @@ public:
PublicKey&
operator=(PublicKey const& other);
/** Create a public key.
Preconditions:
publicKeyType(slice) != std::nullopt
*/
/** Construct from a raw byte slice.
*
* Copies exactly 33 bytes from `slice` after verifying that the bytes
* represent a recognized key format (secp256k1 or Ed25519). Calls
* `LogicError` — terminating the process — if the slice is undersized
* or does not pass `publicKeyType()`.
*
* @param slice Raw bytes to construct from; must satisfy
* `publicKeyType(slice) != std::nullopt`.
* @note Use `publicKeyType()` to validate untrusted input before
* constructing; `parseBase58<PublicKey>` does this automatically
* for Base58-encoded keys.
*/
explicit PublicKey(Slice const& slice);
/** Return a pointer to the raw 33-byte key buffer. */
[[nodiscard]] std::uint8_t const*
data() const noexcept
{
return buf_;
}
/** Return the fixed size of all `PublicKey` objects (always 33). */
static std::size_t
size() noexcept
{
return kSIZE;
}
/** Return an iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
begin() const noexcept
{
return buf_;
}
/** Return a const iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
cbegin() const noexcept
{
return buf_;
}
/** Return an iterator past the last byte of the key buffer. */
[[nodiscard]] const_iterator
end() const noexcept
{
return buf_ + kSIZE;
}
/** Return a const iterator past the last byte of the key buffer. */
[[nodiscard]] const_iterator
cend() const noexcept
{
return buf_ + kSIZE;
}
/** Return a `Slice` view over the 33-byte key buffer. */
[[nodiscard]] Slice
slice() const noexcept
{
return {buf_, kSIZE};
}
/** Implicit conversion to `Slice` for use with serialization APIs. */
operator Slice() const noexcept
{
return slice();
}
};
/** Print the public key to a stream.
*/
/** Write the public key as a hex string to a stream. */
std::ostream&
operator<<(std::ostream& os, PublicKey const& pk);
/** Return `true` if both keys hold identical 33-byte representations. */
inline bool
operator==(PublicKey const& lhs, PublicKey const& rhs)
{
return std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0;
}
/** Return `true` if `lhs` is lexicographically less than `rhs`. */
inline bool
operator<(PublicKey const& lhs, PublicKey const& rhs)
{
@@ -129,6 +142,15 @@ operator<(PublicKey const& lhs, PublicKey const& rhs)
lhs.data(), lhs.data() + lhs.size(), rhs.data(), rhs.data() + rhs.size());
}
/** Feed the raw 33-byte key into a hash algorithm.
*
* Enables `PublicKey` to be used as a key in unordered containers via
* `boost::hash` or any other `hash_append`-compatible hasher.
*
* @tparam Hasher A `hash_append`-compatible hasher type.
* @param h The hasher to feed bytes into.
* @param pk The key whose bytes are appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, PublicKey const& pk)
@@ -136,6 +158,13 @@ hash_append(Hasher& h, PublicKey const& pk)
h(pk.data(), pk.size());
}
/** Serialization bridge between `STBlob` fields and `PublicKey` values.
*
* This specialization plugs `PublicKey` into XRPL's typed serialization
* framework. It allows `get<PublicKey>` and `set<PublicKey>` on `STBlob`
* fields in serialized ledger objects and transactions without any
* conversion boilerplate at call sites.
*/
template <>
struct STExchange<STBlob, PublicKey>
{
@@ -143,12 +172,14 @@ struct STExchange<STBlob, PublicKey>
using value_type = PublicKey;
/** Read a `PublicKey` from an `STBlob` field into `t`. */
static void
get(std::optional<value_type>& t, STBlob const& u)
{
t.emplace(Slice(u.data(), u.size()));
}
/** Write a `PublicKey` into a new `STBlob` for the given field. */
static std::unique_ptr<STBlob>
set(SField const& f, PublicKey const& t)
{
@@ -158,55 +189,86 @@ struct STExchange<STBlob, PublicKey>
//------------------------------------------------------------------------------
/** Encode a public key as a Base58Check string with a token-type prefix.
*
* @param type The `TokenType` prefix to use (e.g. `TokenType::NodePublic`
* for validator keys, `TokenType::AccountPublic` for signing keys).
* @param pk The key to encode.
* @return The Base58Check-encoded string.
*/
inline std::string
toBase58(TokenType type, PublicKey const& pk)
{
return encodeBase58Token(type, pk.data(), pk.size());
}
/** Decode a Base58Check-encoded public key.
*
* Validates the token-type prefix and that the decoded bytes represent a
* recognized key format. Safe to call on untrusted input.
*
* @param type The expected `TokenType` prefix.
* @param s The Base58Check-encoded string to decode.
* @return A `PublicKey` on success, or `std::nullopt` if the string is
* malformed, uses the wrong token type, or the decoded bytes are not
* a valid secp256k1 or Ed25519 key.
*/
template <>
std::optional<PublicKey>
parseBase58(TokenType type, std::string const& s);
enum class ECDSACanonicality { Canonical, FullyCanonical };
/** Canonicality level of a DER-encoded secp256k1 ECDSA signature.
*
* For any signed message, both `(R, S)` and `(R, G-S)` are mathematically
* valid ECDSA signatures (where G is the secp256k1 curve order). Accepting
* both enables transaction malleability attacks. XRPL prevents this by
* requiring *fully canonical* signatures — where `S ≤ G-S` — for new
* transactions.
*/
enum class ECDSACanonicality {
/** Both R and S are in `[1, G)` with no redundant zero padding, but
* `S > G/2`. Structurally valid; may be accepted in legacy contexts. */
Canonical,
/** Both R and S are in `[1, G)` and `S ≤ G-S`, making the signature
* unique and immune to the malleability flip. Required for new XRPL
* transactions. */
FullyCanonical
};
/** Determines the canonicality of a signature.
A canonical signature is in its most reduced form.
For example the R and S components do not contain
additional leading zeroes. However, even in
canonical form, (R,S) and (R,G-S) are both
valid signatures for message M.
Therefore, to prevent malleability attacks we
define a fully canonical signature as one where:
R < G - S
where G is the curve order.
This routine returns std::nullopt if the format
of the signature is invalid (for example, the
points are encoded incorrectly).
@return std::nullopt if the signature fails
validity checks.
@note Only the format of the signature is checked,
no verification cryptography is performed.
*/
/** Determine the canonicality of a DER-encoded secp256k1 ECDSA signature.
*
* Validates the DER structure (`0x30 <len> 0x02 <R> 0x02 <S>`), checks
* that R and S are properly encoded integers (no negative encoding, no
* redundant zero padding), and compares them against the secp256k1 curve
* order G. Returns `FullyCanonical` when `S ≤ G-S`, `Canonical` when
* `S > G-S` but the signature is otherwise structurally sound.
*
* @param sig DER-encoded ECDSA signature to examine.
* @return `ECDSACanonicality::FullyCanonical` if `S ≤ G-S`,
* `ECDSACanonicality::Canonical` if `S > G-S` but structurally valid,
* or `std::nullopt` if the encoding is malformed (wrong header bytes,
* invalid integer components, R or S outside the curve order, or
* trailing bytes present).
* @note Only the structure and canonicality of the encoding are checked;
* no cryptographic verification is performed.
*/
std::optional<ECDSACanonicality>
ecdsaCanonicality(Slice const& sig);
/** Returns the type of public key.
@return std::nullopt If the public key does not
represent a known type.
*/
/** Determine the algorithm encoded in a public key.
*
* Uses the lead byte as a self-describing type tag: `0xED` → Ed25519;
* `0x02`/`0x03` → secp256k1 compressed. Any other lead byte, or a slice
* that is not exactly 33 bytes, is unrecognized.
*
* @return The detected `KeyType`, or `std::nullopt` if the bytes do not
* match a known key format.
*/
/** @{ */
[[nodiscard]] std::optional<KeyType>
publicKeyType(Slice const& slice);
/** @copydoc publicKeyType(Slice const&) */
[[nodiscard]] inline std::optional<KeyType>
publicKeyType(PublicKey const& publicKey)
{
@@ -214,7 +276,24 @@ publicKeyType(PublicKey const& publicKey)
}
/** @} */
/** Verify a secp256k1 signature on the digest of a message. */
/** Verify a secp256k1 ECDSA signature against a pre-computed digest.
*
* Validates DER structure and canonicality before calling libsecp256k1.
* When `mustBeFullyCanonical` is `false` and the signature is merely
* canonical (S > G/2), the S component is normalized to its low form via
* `secp256k1_ecdsa_signature_normalize` before verification — preserving
* backward compatibility without accepting truly malformed encodings.
*
* @param publicKey A secp256k1 public key. Passing an Ed25519 key calls
* `LogicError` (programming error).
* @param digest The 256-bit digest over which the signature was produced.
* @param sig DER-encoded ECDSA signature.
* @param mustBeFullyCanonical If `true` (default), reject signatures where
* `S > G/2`. If `false`, accept them after S normalization.
* @return `true` if the signature is cryptographically valid for the given
* key and digest; `false` for any structural, canonicality, or
* cryptographic failure.
*/
[[nodiscard]] bool
verifyDigest(
PublicKey const& publicKey,
@@ -222,22 +301,65 @@ verifyDigest(
Slice const& sig,
bool mustBeFullyCanonical = true) noexcept;
/** Verify a signature on a message.
With secp256k1 signatures, the data is first hashed with
SHA512-Half, and the resulting digest is signed.
*/
/** Verify a signature over a raw message for either supported key type.
*
* Dispatches on the cryptosystem detected from `publicKey`:
* - **secp256k1**: hashes `m` with SHA512-Half (256-bit digest) and
* delegates to `verifyDigest` with `mustBeFullyCanonical = true`.
* - **Ed25519**: checks that the signature scalar S is below the Ed25519
* subgroup order, then calls the underlying `ed25519_sign_open` library
* after stripping the XRPL-specific `0xED` prefix byte that the library
* does not understand.
*
* @param publicKey The public key to verify against.
* @param m The message that was signed (raw bytes, not pre-hashed).
* @param sig The signature to verify.
* @return `true` if the signature is valid; `false` for any failure
* including unrecognized key type, non-canonical signature, or
* cryptographic mismatch.
*/
[[nodiscard]] bool
verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept;
/** Calculate the 160-bit node ID from a node public key. */
/** Derive the 160-bit node identity from a public key.
*
* Applies RIPEMD-160(SHA-256(pubkey)) to produce the `NodeID` used in the
* peer-to-peer layer for validator routing and consensus tracking.
*
* @param pk The validator's public key (secp256k1 or Ed25519).
* @return The 160-bit `NodeID` identifying the validator on the network.
*/
NodeID
calcNodeID(PublicKey const&);
/** Derive the 160-bit on-ledger account address from a public key.
*
* Applies RIPEMD-160(SHA-256(pubkey)) — the same algorithm used in
* Bitcoin — to produce the `AccountID` that identifies the account on the
* XRP Ledger.
*
* @param pk The account's public key.
* @return The `AccountID` corresponding to `pk`.
* @note The implementation lives in `AccountID.cpp` rather than
* `PublicKey.cpp` due to header dependency ordering constraints.
*/
// VFALCO This belongs in AccountID.h but
// is here because of header issues
AccountID
calcAccountID(PublicKey const& pk);
/** Format a human-readable peer fingerprint for diagnostic logging.
*
* Produces a string of the form
* `"IP Address: <addr>[, Public Key: <NodePublic>][, Id: <id>]"` suitable
* for audit and connection-lifecycle log messages.
*
* @param address The peer's IP endpoint (always included).
* @param publicKey The peer's node public key, encoded as `NodePublic`
* Base58; omitted if not yet known (e.g., before the handshake).
* @param id An optional session identifier string; omitted if absent.
* @return A formatted fingerprint string.
*/
inline std::string
getFingerprint(
beast::IP::Endpoint const& address,
@@ -260,7 +382,22 @@ getFingerprint(
//------------------------------------------------------------------------------
namespace json {
/** Deserialize a `PublicKey` from a JSON field value.
*
* Accepts three formats in order:
* 1. Lowercase hex string of the raw 33-byte key.
* 2. `NodePublic` Base58Check encoding (validator keys).
* 3. `AccountPublic` Base58Check encoding (signing keys).
*
* This covers the variety of formats that appear in RPC requests and
* configuration files.
*
* @param v The JSON object to read from.
* @param field The field whose value is decoded.
* @return The decoded `PublicKey`.
* @throws `JsonTypeMismatchError` if the field value does not match any
* recognized format.
*/
template <>
inline xrpl::PublicKey
getOrThrow(json::Value const& v, xrpl::SField const& field)

View File

@@ -1,3 +1,14 @@
/** @file
* Defines `Quality` and `TAmounts`, the core exchange-rate abstractions
* used by XRPL's on-ledger decentralized exchange (DEX).
*
* `Quality` is the sortable representation of a currency exchange rate.
* The offer-crossing engine — ranking offers, scaling partial fills, and
* composing multi-hop paths — is expressed entirely in terms of these types.
*
* @see QualityFunction.h for the continuous AMM price-function extension.
*/
#pragma once
#include <xrpl/protocol/AmountConversions.h>
@@ -12,35 +23,50 @@
namespace xrpl {
/** Represents a pair of input and output currencies.
The input currency can be converted to the output
currency by multiplying by the rate, represented by
Quality.
For offers, "in" is always TakerPays and "out" is
always TakerGets.
*/
/** A typed pair of input and output amounts representing one side of a trade.
*
* For offers on the DEX, `in` is always `TakerPays` and `out` is always
* `TakerGets`. The template parameters allow instantiation over
* `STAmount`, `IOUAmount`, `XRPAmount`, and `MPTAmount`.
*
* @tparam In Type of the input (paying) amount.
* @tparam Out Type of the output (receiving) amount.
*/
template <class In, class Out>
struct TAmounts
{
TAmounts() = default;
/** Construct a zero-valued pair. */
TAmounts(beast::Zero, beast::Zero) : in(beast::kZERO), out(beast::kZERO)
{
}
/** Construct from explicit in and out amounts.
*
* @param in The input (TakerPays) amount.
* @param out The output (TakerGets) amount.
*/
TAmounts(In in, Out out) : in(std::move(in)), out(std::move(out))
{
}
/** Returns `true` if either quantity is not positive. */
/** Returns `true` if either quantity is not positive.
*
* Used by the offer-crossing engine to skip exhausted or invalid offers
* without further computation.
*/
[[nodiscard]] bool
empty() const noexcept
{
return in <= beast::kZERO || out <= beast::kZERO;
}
/** Adds `rhs` component-wise to this pair.
*
* @param rhs The amounts to add.
* @return Reference to `*this`.
*/
TAmounts&
operator+=(TAmounts const& rhs)
{
@@ -49,6 +75,11 @@ struct TAmounts
return *this;
}
/** Subtracts `rhs` component-wise from this pair.
*
* @param rhs The amounts to subtract.
* @return Reference to `*this`.
*/
TAmounts&
operator-=(TAmounts const& rhs)
{
@@ -57,12 +88,14 @@ struct TAmounts
return *this;
}
In in{};
Out out{};
In in{}; /**< Input (TakerPays) amount. */
Out out{}; /**< Output (TakerGets) amount. */
};
/** Canonical `TAmounts` alias used by the `STAmount`-based offer-crossing path. */
using Amounts = TAmounts<STAmount, STAmount>;
/** Returns `true` when both sides of two `TAmounts` pairs are equal. */
template <class In, class Out>
bool
operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
@@ -70,6 +103,7 @@ operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
return lhs.in == rhs.in && lhs.out == rhs.out;
}
/** Returns `true` when either side of two `TAmounts` pairs differs. */
template <class In, class Out>
bool
operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
@@ -79,54 +113,107 @@ operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
//------------------------------------------------------------------------------
// XRPL specific constant used for parsing qualities and other things
/** Unity exchange rate (1:1), scaled to XRPL's 9-decimal fixed-point precision.
*
* Appears throughout offer parsing and fee calculations wherever a 1:1
* exchange rate must be expressed as a raw integer.
*/
#define QUALITY_ONE 1'000'000'000
/** Represents the logical ratio of output currency to input currency.
Internally this is stored using a custom floating point representation,
as the inverse of the ratio, so that quality will be descending in
a sequence of actual values that represent qualities.
*/
/** The exchange rate of an offer, stored as an inverted packed floating-point
* integer so that higher-quality offers sort first under plain integer comparison.
*
* A `Quality` encodes the ratio `out / in` (TakerGets / TakerPays): how much
* output the taker receives per unit of input. Higher quality is better for
* the taker (more output per unit of input).
*
* The internal `uint64_t` uses the same bit layout as `STAmount` IOU encoding:
* the top 8 bits hold a biased exponent (stored value = actual exponent + 100)
* and the lower 56 bits hold an unsigned mantissa. Critically, the integer
* value is **inverted** relative to the economic concept — a *higher* quality
* corresponds to a *lower* `uint64_t` — so that ascending integer order in the
* ledger's offer directories corresponds to descending quality, allowing the
* best offers to be processed first.
*
* @note The increment/decrement operators navigate the discrete floating-point
* grid by modifying `value_` by one ULP. The representation may become
* non-canonical after such operations.
*
* @see composedQuality() for two-hop path composition.
* @see QualityFunction.h for the continuous AMM extension of this type.
*/
class Quality
{
public:
// Type of the internal representation. Higher qualities
// have lower unsigned integer representations.
/** Underlying storage type. Higher qualities have lower integer values. */
using value_type = std::uint64_t;
/** Minimum valid tick size (significant decimal digits) for `round()`. */
static int const kMIN_TICK_SIZE = 3;
/** Maximum valid tick size (significant decimal digits) for `round()`. */
static int const kMAX_TICK_SIZE = 16;
private:
// This has the same representation as STAmount, see the comment on the
// STAmount. However, this class does not always use the canonical
// representation. In particular, the increment and decrement operators may
// cause a non-canonical representation.
// Packed 64-bit encoding: bits [63:56] = biased exponent (actual + 100),
// bits [55:0] = mantissa. Identical to the STAmount IOU wire format.
// May be non-canonical after operator++ / operator--.
value_type value_;
public:
Quality() = default;
/** Create a quality from the integer encoding of an STAmount */
/** Construct from a raw packed integer in STAmount encoding.
*
* The top 8 bits are the biased exponent (actual exponent + 100) and
* the bottom 56 bits are the mantissa. Higher integers denote lower
* (worse) quality because the internal ordering is inverted.
*
* @param value Packed 64-bit quality value.
*/
explicit Quality(std::uint64_t value);
/** Create a quality from the ratio of two amounts. */
/** Construct from an `STAmount` in/out pair encoding `out / in`.
*
* Calls `getRate(amount.out, amount.in)` to produce the packed value.
* Neither side should be zero.
*
* @param amount Offer amounts: `in` = TakerPays, `out` = TakerGets.
*/
explicit Quality(Amounts const& amount);
/** Create a quality from the ratio of two amounts. */
/** Construct from a typed in/out pair by converting to `STAmount` first.
*
* @tparam In Input amount type (e.g., `XRPAmount`, `IOUAmount`).
* @tparam Out Output amount type.
* @param amount The typed offer amounts.
*/
template <class In, class Out>
explicit Quality(TAmounts<In, Out> const& amount)
: Quality(Amounts(toSTAmount(amount.in), toSTAmount(amount.out)))
{
}
/** Create a quality from the ratio of two amounts. */
/** Construct from explicit out and in amounts by converting to `STAmount`.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param out The output (TakerGets) amount.
* @param in The input (TakerPays) amount.
*/
template <class In, class Out>
Quality(Out const& out, In const& in) : Quality(Amounts(toSTAmount(in), toSTAmount(out)))
{
}
/** Advances to the next higher quality level. */
/** Advance to the next higher quality level.
*
* Because the internal encoding is inverted, this decrements the stored
* integer by one ULP. Used during offer-book traversal to step the
* crossing price up by the smallest representable increment.
*
* @pre `value_ > 0`; underflow is asserted.
*/
/** @{ */
Quality&
operator++();
@@ -135,7 +222,13 @@ public:
operator++(int);
/** @} */
/** Advances to the next lower quality level. */
/** Retreat to the next lower quality level.
*
* Because the internal encoding is inverted, this increments the stored
* integer by one ULP.
*
* @pre `value_ < UINT64_MAX`; overflow is asserted.
*/
/** @{ */
Quality&
operator--();
@@ -144,65 +237,184 @@ public:
operator--(int);
/** @} */
/** Returns the quality as STAmount. */
/** Decode the packed quality value into an `STAmount` exchange rate.
*
* The returned amount represents the rate `out / in` in the IOU
* floating-point format. Callers use this when passing the quality
* to `mulRound` / `divRound` for proportional scaling.
*
* @return The exchange rate as an `STAmount`.
*/
[[nodiscard]] STAmount
rate() const
{
return amountFromQuality(value_);
}
/** Returns the quality rounded up to the specified number
of decimal digits.
*/
/** Round the quality's mantissa up to `tickSize` significant decimal digits.
*
* Used for tick-size enforcement: coarsens the price grid so that offers
* differing only in low-order digits are treated as equivalent. Rounding
* is always upward (ceiling), which makes the encoded rate slightly higher
* (worse for the taker) and prevents a rounded quality from being mistakenly
* ranked better than the original.
*
* @param tickSize Number of significant digits to retain. Must be in
* `[kMIN_TICK_SIZE, kMAX_TICK_SIZE]`; enforcement is the caller's
* responsibility.
* @return A new `Quality` with a rounded-up mantissa and unchanged exponent.
*/
[[nodiscard]] Quality
round(int tickSize) const;
/** Returns the scaled amount with in capped.
Math is avoided if the result is exact. The output is clamped
to prevent money creation.
*/
/** Scale an offer's amounts down so that the input does not exceed `limit`.
*
* If `amount.in > limit`, sets `in = limit` and recomputes `out`
* proportionally via `divRound`. The computed output is clamped to
* `amount.out` if arithmetic would produce a larger value, preventing
* money creation due to rounding. Returns `amount` unchanged when
* `amount.in <= limit`.
*
* @param amount Current offer amounts (`in` = TakerPays, `out` = TakerGets).
* @param limit Maximum allowed input amount.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
* @note Uses `divRound` (legacy rounding that ignores low-order bits).
* Use `ceilInStrict` when full-precision rounding is required.
*/
[[nodiscard]] Amounts
ceilIn(Amounts const& amount, STAmount const& limit) const;
/** Scale a typed offer's amounts down so that the input does not exceed `limit`.
*
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
* then converts the result back to the typed amounts.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilIn(TAmounts<In, Out> const& amount, In const& limit) const;
// Some of the underlying rounding functions called by ceil_in() ignored
// low order bits that could influence rounding decisions. This "strict"
// method uses underlying functions that pay attention to all the bits.
/** Scale an offer's amounts down so that the input does not exceed `limit`,
* using full-precision rounding.
*
* Identical to `ceilIn` except it delegates to `divRoundStrict`, which
* considers all low-order bits that `divRound` ignores. Introduced to
* fix subtle rounding bugs where a borderline result could influence
* whether an offer crosses.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @param roundUp Whether to round the recomputed output up (`true`) or
* down (`false`).
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
[[nodiscard]] Amounts
ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
/** Scale a typed offer's amounts down so that the input does not exceed `limit`,
* using full-precision rounding.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @param roundUp Whether to round the recomputed output up or down.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const;
/** Returns the scaled amount with out capped.
Math is avoided if the result is exact. The input is clamped
to prevent money creation.
*/
/** Scale an offer's amounts down so that the output does not exceed `limit`.
*
* If `amount.out > limit`, sets `out = limit` and recomputes `in`
* proportionally via `mulRound`. The computed input is clamped to
* `amount.in` if arithmetic would produce a larger value, preventing
* money creation due to rounding. Returns `amount` unchanged when
* `amount.out <= limit`.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
* @note Uses `mulRound` (legacy rounding that ignores low-order bits).
* Use `ceilOutStrict` when full-precision rounding is required.
*/
[[nodiscard]] Amounts
ceilOut(Amounts const& amount, STAmount const& limit) const;
/** Scale a typed offer's amounts down so that the output does not exceed `limit`.
*
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
* then converts the result back to the typed amounts.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const;
// Some of the underlying rounding functions called by ceil_out() ignored
// low order bits that could influence rounding decisions. This "strict"
// method uses underlying functions that pay attention to all the bits.
/** Scale an offer's amounts down so that the output does not exceed `limit`,
* using full-precision rounding.
*
* Identical to `ceilOut` except it delegates to `mulRoundStrict`, which
* considers all low-order bits that `mulRound` ignores.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @param roundUp Whether to round the recomputed input up (`true`) or
* down (`false`).
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
[[nodiscard]] Amounts
ceilOutStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
/** Scale a typed offer's amounts down so that the output does not exceed `limit`,
* using full-precision rounding.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @param roundUp Whether to round the recomputed input up or down.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const;
private:
// The ceil_in and ceil_out methods that deal in TAmount all convert
// their arguments to STAmount and convert the result back to TAmount.
// This helper function takes care of all the conversion operations.
/** Shared implementation for all typed `ceilIn`/`ceilOut` overloads.
*
* Converts `amount` and `limit` to `STAmount`, calls `ceilFunction` (a
* member-function pointer to one of the `STAmount`-based overloads), and
* converts the result back to `TAmounts<In, Out>`. Returns `amount`
* unchanged when `limitCmp <= limit` (i.e., the limit is not binding).
*
* The variadic `Round...` pack forwards an optional `bool roundUp` argument
* to strict variants without requiring separate instantiations.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @tparam Lim Limit amount type (same as `In` or `Out`).
* @tparam FnPtr Pointer to the `STAmount`-based ceil member function.
* @tparam Round Empty or `{bool}` — forwarded as `roundUp`.
* @param amount Current typed offer amounts.
* @param limit The cap to apply.
* @param limitCmp The side of `amount` to compare against `limit`
* (either `amount.in` or `amount.out`).
* @param ceilFunction Member-function pointer to dispatch to.
* @param round Optional rounding direction (strict variants only).
* @return Scaled `TAmounts<In, Out>`.
*/
template <class In, class Out, class Lim, typename FnPtr, std::same_as<bool>... Round>
[[nodiscard]] TAmounts<In, Out>
ceilTAmountsHelper(
@@ -213,46 +425,53 @@ private:
Round... round) const;
public:
/** Returns `true` if lhs is lower quality than `rhs`.
Lower quality means the taker receives a worse deal.
Higher quality is better for the taker.
*/
/** Returns `true` if `lhs` is lower quality (worse for the taker) than `rhs`.
*
* Because the internal encoding is inverted, a lower quality corresponds
* to a *higher* stored integer, so this compares `lhs.value_ > rhs.value_`.
*/
friend bool
operator<(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ > rhs.value_;
}
/** Returns `true` if `lhs` is higher quality (better for the taker) than `rhs`. */
friend bool
operator>(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ < rhs.value_;
}
/** Returns `true` if `lhs` is lower or equal quality to `rhs`. */
friend bool
operator<=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs > rhs);
}
/** Returns `true` if `lhs` is higher or equal quality to `rhs`. */
friend bool
operator>=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs < rhs);
}
/** Returns `true` if both qualities encode the same exchange rate. */
friend bool
operator==(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ == rhs.value_;
}
/** Returns `true` if the two qualities encode different exchange rates. */
friend bool
operator!=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs == rhs);
}
/** Write the raw packed integer value of the quality to an output stream. */
friend std::ostream&
operator<<(std::ostream& os, Quality const& quality)
{
@@ -260,8 +479,18 @@ public:
return os;
}
// return the relative distance (relative error) between two qualities. This
// is used for testing only. relative distance is abs(a-b)/min(a,b)
/** Return the relative error between two quality values: `|a - b| / min(a, b)`.
*
* Extracts the exponent and mantissa from each packed value, scales them
* to a common exponent, and returns the normalized distance. Used only
* in unit tests to verify that two qualities are sufficiently close.
*
* @param q1 First quality; must be non-zero (asserted).
* @param q2 Second quality; must be non-zero (asserted).
* @return `|q1 - q2| / min(q1, q2)` as a `double`.
* @note For testing only. Production code should compare with the
* relational operators.
*/
friend double
relativeDistance(Quality const& q1, Quality const& q2)
{
@@ -284,9 +513,8 @@ public:
double const maxVD =
(expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast<double>(maxVMantissa);
// maxVD and minVD are scaled so they have the same exponents. Dividing
// cancels out the exponents, so we only need to deal with the (scaled)
// mantissas
// maxVD and minVD are scaled so they have the same exponent; dividing
// cancels out the exponents, leaving only the normalized mantissa difference.
return (maxVD - minVD) / minVD;
}
};
@@ -315,7 +543,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilIn(TAmounts<In, Out> const& amount, In const& limit) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&) const =
&Quality::ceilIn;
@@ -326,7 +553,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&, bool)
const = &Quality::ceilInStrict;
@@ -337,7 +563,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&) const =
&Quality::ceilOut;
@@ -348,17 +573,26 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&, bool)
const = &Quality::ceilOutStrict;
return ceilTAmountsHelper(amount, limit, amount.out, kCEIL_OUT_FN_PTR, roundUp);
}
/** Calculate the quality of a two-hop path given the two hops.
@param lhs The first leg of the path: input to intermediate.
@param rhs The second leg of the path: intermediate to output.
*/
/** Compute the effective end-to-end exchange rate for a two-hop path.
*
* If the first hop converts A→B at rate `lhs` and the second converts B→C
* at rate `rhs`, the composed quality is their product, re-encoded into the
* packed 64-bit format. Used by the pathfinding engine to rank multi-hop
* routes against single-hop offers on a common scale.
*
* @param lhs Quality of the first leg (input → intermediate currency).
* @param rhs Quality of the second leg (intermediate → output currency).
* @return Composed quality representing the overall A→C exchange rate.
* @note Both input rates must be non-zero (asserted at runtime). The
* composed exponent must fit in 8 bits (i.e., actual exponent in
* [-99, 155]); astronomically large or small paths will assert.
*/
Quality
composedQuality(Quality const& lhs, Quality const& rhs);

View File

@@ -6,52 +6,152 @@
namespace xrpl {
/** Average quality of a path as a function of `out`: q(out) = m * out + b,
* where m = -1 / poolGets, b = poolPays / poolGets. If CLOB offer then
* `m` is equal to 0 `b` is equal to the offer's quality. The function
* is derived by substituting `in` in q = out / in with the swap out formula
* for `in`:
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / (1 - tfee)
* and combining the function for multiple steps. The function is used
* to limit required output amount when quality limit is provided in one
* path optimization.
/** Average quality of a payment strand expressed as a linear function of output.
*
* Models the relationship `q(out) = m_ * out + b_`, where `q` is the average
* exchange rate (quality) that a strand delivers when it produces `out` units.
* This analytical model lets `StrandFlow::limitOut()` compute — without
* simulation — the maximum output the strand may produce before AMM price
* impact degrades the average quality below a caller-supplied limit.
*
* **Derivation.** For an AMM step with pool balances `poolGets` (input side)
* and `poolPays` (output side) and fee multiplier `cfee = 1 - tfee`, the
* constant-product swap formula gives:
* @code
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / cfee
* @endcode
* Substituting into `q = out / in` and linearising yields:
* @code
* m = -cfee / poolGets (always negative for a valid AMM step)
* b = poolPays * cfee / poolGets
* @endcode
*
* **Multi-hop composition.** For strands with sequential steps (e.g. a
* transfer-fee hop preceding an AMM hop), `combine()` chains two quality
* functions analytically. `StrandFlow::limitOut()` calls `combine()` in a
* loop over all steps to accumulate a single QF representing the whole strand.
*
* **Two construction modes** are selected via tag dispatch:
* - `AMMTag` — variable quality; slope and intercept derived from pool balances.
* - `CLOBLikeTag` — constant quality (`m_ = 0`); used for plain CLOB orders and
* for AMM offers in multi-path mode, where each path's AMM allocation is fixed.
*
* @note The linear approximation is exact for *average* quality but not for
* instantaneous (marginal) quality, which is quadratic. Using averages
* keeps composition algebraically simple while still providing a
* conservative, analytically tractable bound.
*
* @see StrandFlow.h `limitOut()` — primary consumer of this class.
*/
class QualityFunction
{
private:
// slope
/** Slope of the qualityoutput line (`-cfee / poolGets` for AMM; 0 for CLOB). */
Number m_;
// intercept
/** Intercept of the qualityoutput line (`poolPays * cfee / poolGets` for AMM;
* `1 / quality.rate()` for CLOB). */
Number b_;
// seated if QF is for CLOB offer.
/** Cached constant quality; seated only when `m_ == 0` (CLOB-like function). */
std::optional<Quality> quality_;
public:
/** Tag type that selects the AMM constructor (variable-quality path step). */
struct AMMTag
{
};
// AMMOffer for multi-path is like CLOB, i.e. the offer size
// changes proportionally to its quality.
/** Tag type that selects the CLOB-like constructor (constant-quality path step).
*
* Used for both plain CLOB orders and AMM offers operating in multi-path
* mode, where the AMM offer size scales proportionally with quality just
* like a CLOB, making the effective quality constant from this sub-path's
* perspective.
*/
struct CLOBLikeTag
{
};
/** Construct a constant-quality (CLOB-like) quality function.
*
* Sets `m_ = 0` and `b_ = 1 / quality.rate()`. `quality_` is seated so
* that `isConst()` returns `true` and `StrandFlow::limitOut()` skips the
* output cap.
*
* @param quality The fixed exchange rate of this path step.
* @throws std::runtime_error if `quality.rate()` is zero, which would
* make the intercept infinite.
*/
QualityFunction(Quality const& quality, CLOBLikeTag);
/** Construct a variable-quality (AMM) quality function from pool balances.
*
* Derives the slope and intercept from the constant-product swap formula:
* @code
* m_ = -cfee / amounts.in
* b_ = amounts.out * cfee / amounts.in
* @endcode
* where `cfee = feeMult(tfee)`. `quality_` is left empty; `isConst()`
* returns `false`.
*
* @tparam TIn Input amount type (e.g. `XRPAmount`, `IOUAmount`).
* @tparam TOut Output amount type.
* @param amounts Current AMM pool balances: `amounts.in` is the input-side
* pool depth, `amounts.out` is the output-side pool depth.
* @param tfee AMM trading fee in the same units as `feeMult()` expects.
* @throws std::runtime_error if either pool balance is zero or negative,
* which would cause division-by-zero in downstream arithmetic.
*/
template <typename TIn, typename TOut>
QualityFunction(TAmounts<TIn, TOut> const& amounts, std::uint32_t tfee, AMMTag);
/** Combines QF with the next step QF
/** Chain this quality function with the next path step's quality function.
*
* Applies linear function composition in reciprocal-rate space:
* @code
* m_ += b_ * qf.m_;
* b_ *= qf.b_;
* @endcode
* If the combined slope becomes nonzero, `quality_` is cleared to reflect
* that the resulting function is no longer constant and `outFromAvgQ()`
* must be used rather than a simple pass/fail quality check.
*
* @param qf Quality function for the next step to compose in.
*/
void
combine(QualityFunction const& qf);
/** Find output to produce the requested
* average quality.
* @param quality requested average quality (quality limit)
/** Solve for the maximum output at which average quality meets the given limit.
*
* Inverts `q(out) = m_ * out + b_` by substituting `q = 1 / quality.rate()`:
* @code
* out = (1 / quality.rate() - b_) / m_
* @endcode
* The rounding mode is set to `Upward` during the calculation so the
* returned bound is conservative: because `m_` is negative, dividing an
* upward-rounded numerator by a negative slope yields a result that rounds
* down, ensuring the engine never requests marginally more output than the
* quality constraint allows.
*
* Returns `std::nullopt` in three cases:
* - `m_ == 0`: the function is constant (CLOB-like); quality either passes
* or fails uniformly, so no output cap is meaningful.
* - `quality.rate() == 0`: guards against division-by-zero when forming
* `1 / rate`.
* - `out <= 0`: the quality limit cannot be achieved at any positive output;
* the strand is effectively dead for this constraint.
*
* @param quality The minimum acceptable average exchange rate (quality limit).
* @return The output amount at which the strand's average quality equals
* `quality`, or `std::nullopt` if the cap is inapplicable or infeasible.
*/
std::optional<Number>
outFromAvgQ(Quality const& quality);
/** Return true if the quality function is constant
/** Return `true` if this quality function is constant (CLOB-like).
*
* A constant function has `m_ == 0`: the average quality is the same
* regardless of output size. `StrandFlow::limitOut()` treats a constant
* function as a signal to skip the output cap and return `remainingOut`
* unchanged.
*/
[[nodiscard]] bool
isConst() const
@@ -59,6 +159,13 @@ public:
return quality_.has_value();
}
/** Return the cached constant quality, if any.
*
* Seated only when `isConst() == true` (i.e., this is a CLOB-like
* function constructed via `CLOBLikeTag`). Returns `std::nullopt` for
* variable-quality (AMM) functions and for any combined function whose
* slope became nonzero after `combine()`.
*/
[[nodiscard]] std::optional<Quality> const&
quality() const
{

View File

@@ -1,13 +1,49 @@
#pragma once
/** @file
* Deprecated compatibility shim for the XRPL RPC error API.
*
* Declares `rpcError()` and `isRpcError()` — legacy entry points in the
* `xrpl` namespace that predate the richer `RPC`-namespaced error
* infrastructure in `ErrorCodes.h`. New code should use `RPC::makeError()`
* and `RPC::containsError()` from `ErrorCodes.h` directly.
*/
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
namespace xrpl {
// VFALCO NOTE these are deprecated
/** Return `true` if @p jvResult represents an RPC error response.
*
* Duck-types the JSON value by checking for the presence of the `"error"`
* key — the same structural sentinel that `RPC::containsError()` tests,
* making it the direct modern equivalent.
*
* @param jvResult The JSON value to inspect (taken by value, not
* `const` reference — an inefficiency inherited from the original
* implementation that was never corrected given this function's
* deprecated status).
* @return `true` if @p jvResult is a JSON object containing an `"error"`
* member.
* @deprecated Use `RPC::containsError(json)` from `ErrorCodes.h` instead.
*/
bool
isRpcError(json::Value jvResult);
/** Construct a fresh JSON error object for the given error code.
*
* Delegates to `RPC::injectError()`, which populates a new `Json::Value`
* object with the canonical `error` token, `error_code`, and
* `error_message` fields drawn from the static `ErrorInfo` registry.
* The return-by-value produces a self-contained error object ready for
* direct return from an RPC handler.
*
* @param iError The RPC error code to encode.
* @return A new `Json::Value` object with `error`, `error_code`, and
* `error_message` populated.
* @deprecated Use `RPC::makeError(code)` from `ErrorCodes.h` instead.
*/
json::Value
rpcError(ErrorCodeI iError);

View File

@@ -1,3 +1,13 @@
/** @file
* Defines the `Rate` struct and its arithmetic free functions for applying
* XRPL transfer fees to `STAmount` values.
*
* Transfer rates are billion-scale fractions: `1,000,000,000` is parity
* (no fee). `kPARITY_RATE` is the sentinel for the fee-free common case;
* all six arithmetic functions short-circuit on it without entering the
* `STAmount` multiply/divide path.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -10,14 +20,27 @@
namespace xrpl {
/** Represents a transfer rate
Transfer rates are specified as fractions of 1 billion.
For example, a transfer rate of 1% is represented as
1,010,000,000.
*/
/** Protocol-level transfer rate, expressed as a fraction of one billion.
*
* A value of `1,000,000,000` means 1:1 — no fee. A value of
* `1,010,000,000` means the sender must deliver 1.01 units for every 1 unit
* the recipient receives (a 1% fee). This scale matches `QUALITY_ONE` in
* `Quality.h`, tying transfer fees directly to the ledger's price
* representation.
*
* `boost::totally_ordered<Rate>` generates `!=`, `>`, `<=`, and `>=` from
* the manually provided `==` and `<`, keeping the struct concise while
* remaining fully ordered.
*
* @note The default constructor is deleted: a `Rate` with an unspecified
* value is meaningless, and zero would violate the nonzero precondition
* asserted by every arithmetic function in this header. The constructor
* is `explicit` to prevent accidental implicit conversion from the
* large integers that rate values resemble.
*/
struct Rate : private boost::totally_ordered<Rate>
{
/** The raw billion-scale rate value as stored in `sfTransferRate`. */
std::uint32_t value;
Rate() = delete;
@@ -27,18 +50,21 @@ struct Rate : private boost::totally_ordered<Rate>
}
};
/** Returns `true` if both rates have the same billion-scale value. */
inline bool
operator==(Rate const& lhs, Rate const& rhs) noexcept
{
return lhs.value == rhs.value;
}
/** Returns `true` if `lhs` is a strictly smaller rate than `rhs`. */
inline bool
operator<(Rate const& lhs, Rate const& rhs) noexcept
{
return lhs.value < rhs.value;
}
/** Writes the raw billion-scale rate value to `os`. */
inline std::ostream&
operator<<(std::ostream& os, Rate const& rate)
{
@@ -46,32 +72,126 @@ operator<<(std::ostream& os, Rate const& rate)
return os;
}
/** Scale an amount by a transfer rate, preserving its asset.
*
* Computes `amount × (rate / 10^9)`. Returns `amount` unchanged when
* `rate == kPARITY_RATE`, avoiding the `STAmount` arithmetic path for the
* common fee-free case.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiply(STAmount const& amount, Rate const& rate);
/** Scale an amount by a transfer rate with controlled rounding, preserving its asset.
*
* Like `multiply()`, but the caller controls rounding direction. Used in
* IOU payment routing where fee calculations stay in a single currency.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp);
/** Scale an amount by a transfer rate with controlled rounding, emitting a specified asset.
*
* Overload for offer-crossing and cross-currency paths where the output
* must be denominated in a different asset than the input.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @param asset The asset type of the returned `STAmount`.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in `asset`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiplyRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
/** Scale an amount by the inverse of a transfer rate, preserving its asset.
*
* Computes `amount / (rate / 10^9)` — the inverse of `multiply()`. Used
* when back-calculating the gross send amount needed to deliver a given net
* amount after fees. Returns `amount` unchanged for `kPARITY_RATE`.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divide(STAmount const& amount, Rate const& rate);
/** Scale an amount by the inverse of a transfer rate with controlled rounding, preserving its asset.
*
* Like `divide()`, but the caller controls rounding direction. Used in
* IOU payment routing for single-currency gross-amount back-calculation.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divideRound(STAmount const& amount, Rate const& rate, bool roundUp);
/** Scale an amount by the inverse of a transfer rate with controlled rounding, emitting a specified asset.
*
* Overload for offer-crossing and cross-currency paths where the output
* must be denominated in a different asset than the input.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @param asset The asset type of the returned `STAmount`.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in `asset`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divideRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
namespace nft {
/** Given a transfer fee (in basis points) convert it to a transfer rate. */
/** Convert an NFT transfer fee in basis points to a billion-scale `Rate`.
*
* NFT royalties are stored as a `uint16_t` in basis points (050,000
* representing 0%50%). Because `Rate` uses `10^9` as its unit, the
* conversion multiplies by `10,000`: a maximum fee of `50,000 bp` becomes
* `500,000,000`, safely below `QUALITY_ONE` and within `uint32_t` range.
*
* @param fee NFT transfer fee in basis points (050,000); validated by
* transaction processing before reaching this function.
* @return A `Rate` suitable for passing to `multiply()` or `multiplyRound()`.
* @note Do not call this for ordinary IOU transfer rates — those are already
* billion-scale and should be wrapped in `Rate` directly.
*/
Rate
transferFeeAsRate(std::uint16_t fee);
} // namespace nft
/** A transfer rate signifying a 1:1 exchange */
/** The 1:1 transfer rate — sender pays exactly what the recipient receives.
*
* Equal to `QUALITY_ONE` (`1,000,000,000`). Every arithmetic function in
* this header returns `amount` unchanged when it detects this value, so
* payment paths through accounts with no transfer fee never enter the
* `STAmount` multiply/divide path. `transferRate()` returns this sentinel
* when an account's `sfTransferRate` field is absent.
*
* @see transferRate() in AccountRootHelpers.h
*/
extern Rate const kPARITY_RATE;
} // namespace xrpl

View File

@@ -1,9 +1,31 @@
/** @file
* Defines the `LedgerHash` type alias used throughout the ledger stack to
* identify closed ledgers by their cryptographic digest.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
namespace xrpl {
/** The SHA-512/256 digest that uniquely identifies a closed XRP Ledger.
*
* A ledger hash is computed over the serialized `LedgerHeader` — covering the
* account-state hash, transaction-set hash, sequence number, close time, drop
* totals, and parent ledger hash — and is 32 bytes wide.
*
* The alias over bare `uint256` serves two purposes: it makes interfaces
* self-documenting at call sites (e.g., `CanonicalTXSet(LedgerHash const&)`),
* and it isolates all ledger-hash usage behind a single name so that a
* tagged variant (`base_uint<256, struct LedgerHashTag>`) can be introduced
* later to prevent cross-domain substitution with transaction hashes or
* account IDs without touching every call site.
*
* @note `LedgerHeader` stores its hash fields as bare `uint256` for historical
* reasons; higher-level APIs (`CanonicalTXSet`, `LedgerHistory`,
* `InboundLedgers`, `RCLValidations`) consistently use this alias.
*/
using LedgerHash = uint256;
} // namespace xrpl

View File

@@ -8,27 +8,55 @@
namespace xrpl {
/** Check whether a feature is enabled in the current ledger rules
/** Query whether a feature is enabled, with an explicit fallback value.
*
* @param feature The feature to be tested.
* @param resultIfNoRules What to return if called from outside a Transactor context.
* Delegates to the thread-local current `Rules` installed by
* `CurrentTransactionRulesGuard`. Use this overload when the desired
* behavior outside a transaction context differs from `false`.
*
* @param feature The amendment ID to test.
* @param resultIfNoRules Value returned when called outside any transaction
* context (i.e. no `CurrentTransactionRulesGuard` is on the call stack).
* @return `true` if the feature is enabled in the current rules;
* `resultIfNoRules` if no rules are installed.
*/
bool
isFeatureEnabled(uint256 const& feature, bool resultIfNoRules);
/** Check whether a feature is enabled in the current ledger rules
/** Query whether a feature is enabled in the current transaction context.
*
* @param feature The feature to be tested.
* Delegates to the thread-local current `Rules` installed by
* `CurrentTransactionRulesGuard`. Lower-level protocol code that cannot
* accept a `Rules` parameter (e.g. `STAmount`, `AMMHelpers`) uses this
* function instead. The implicit reliance on thread-local state means
* callers must ensure a `CurrentTransactionRulesGuard` is active on the
* call stack; calling it outside a transaction context silently returns
* `false`.
*
* Returns false if no global Rules object is available. i.e. Outside of
* a Transactor context
* @param feature The amendment ID to test.
* @return `true` if the feature is enabled; `false` if no rules are
* installed or the feature is absent from the current ledger's
* amendment set.
*/
bool
isFeatureEnabled(uint256 const& feature);
class DigestAwareReadView;
/** Rules controlling protocol behavior. */
/** Authoritative snapshot of which protocol amendments are active for a ledger.
*
* Every behavioral branch in the transaction engine that depends on a
* conditionally-enabled feature gates through `Rules::enabled()`. `Rules` is
* a value type backed by a `shared_ptr<Impl const>` (pimpl), so copying is
* cheap — only an atomic refcount bump — regardless of how many amendments are
* active. Instances are typically constructed via `makeRulesGivenLedger` and
* installed on the call stack by `CurrentTransactionRulesGuard`.
*
* @note The default constructor is deleted. Every `Rules` instance must carry
* an explicit preset set to prevent accidentally propagating a zero-feature
* state into transaction processing.
* @see makeRulesGivenLedger, CurrentTransactionRulesGuard
*/
class Rules
{
private:
@@ -51,11 +79,15 @@ public:
Rules() = delete;
/** Construct an empty rule set.
These are the rules reflected by
the genesis ledger.
*/
/** Construct an empty rule set from a preset collection.
*
* Intended for the genesis ledger, which has no on-ledger amendments yet.
* The preset features are treated as unconditionally enabled and checked
* first by `enabled()`.
*
* @param presets Features that are always enabled regardless of ledger
* state (e.g. features forced on in test or devnet configurations).
*/
explicit Rules(std::unordered_set<uint256, beast::Uhash<>> const& presets);
private:
@@ -77,28 +109,83 @@ private:
presets() const;
public:
/** Returns `true` if a feature is enabled. */
/** Returns `true` if a feature is enabled in this rule set.
*
* Checks the preset collection first (always-on features), then the
* on-ledger amendment set populated from `sfAmendments`.
*
* @param feature The amendment ID to test.
* @return `true` if the feature is in the preset collection or was
* active in the ledger's amendment set at the time this `Rules`
* was constructed.
*/
[[nodiscard]] bool
enabled(uint256 const& feature) const;
/** Returns `true` if two rule sets are identical.
@note This is for diagnostics.
*/
*
* Comparison is O(1) when both instances carry a digest (the common case
* for ledgers with an amendments SLE): differing digests are immediately
* unequal. Two instances without a digest (genesis state) are considered
* equal. An assertion guards against comparing instances with identical
* digests but differing presets.
*
* @note Intended for diagnostics only, not for load-bearing equality
* decisions in transaction processing.
*/
bool
operator==(Rules const&) const;
/** Returns `true` if two rule sets differ.
*
* Derived from `operator==`; see its documentation for comparison
* semantics.
*/
bool
operator!=(Rules const& other) const;
};
/** Returns the active `Rules` for the current thread's transaction context.
*
* The returned reference is valid until the next call to
* `setCurrentTransactionRules` on this thread. Prefer using
* `CurrentTransactionRulesGuard` over calling these functions directly.
*
* @return The currently installed rules, or an empty `optional` if no
* `CurrentTransactionRulesGuard` is on the call stack.
* @see CurrentTransactionRulesGuard
*/
std::optional<Rules> const&
getCurrentTransactionRules();
/** Install `r` as the active rules for the current thread's transaction context.
*
* Beyond storing `r` in the thread-local slot, this function also calls
* `Number::setMantissaScale()` to push the appropriate numeric precision mode:
* `MantissaRange::large` when `featureSingleAssetVault` or
* `featureLendingProtocol` is enabled, `small` otherwise. This push strategy
* avoids per-operation rule lookups inside hot arithmetic paths.
*
* Prefer `CurrentTransactionRulesGuard` over calling this directly, as it
* ensures the previous rules are always restored on scope exit.
*
* @param r The rules to install, or `std::nullopt` to clear the slot.
* @see CurrentTransactionRulesGuard
*/
void
setCurrentTransactionRules(std::optional<Rules> r);
/** RAII class to set and restore the current transaction rules
/** RAII guard that installs a `Rules` into the thread-local transaction context.
*
* The constructor calls `setCurrentTransactionRules` with the supplied rules,
* saving the previously active value. The destructor restores that saved value,
* ensuring the thread-local state is always reset even on exception paths.
* Non-copyable to prevent accidental aliasing of the saved state.
*
* Production callers are `Transactor::operator()` and `applySteps.cpp`; test
* code uses this guard to bracket individual feature checks.
*
* @see setCurrentTransactionRules, getCurrentTransactionRules
*/
class CurrentTransactionRulesGuard
{

View File

@@ -1,3 +1,20 @@
/** @file
* Compile-time field identification and wire-type catalog for XRPL serialized
* objects.
*
* Every data field that can appear in an XRPL transaction, ledger entry,
* validation, or transaction metadata is identified by a singleton `SField`
* instance declared in this header. The `SerializedTypeID` enum defines the
* recognized wire types. `TypedField<T>` adds a compile-time C++ type so
* that callers can interact with fields in a type-safe way.
*
* @note Some fields distinguish between the default value and the absent
* state. For example, `sfQualityIn` on a trust line with value 0
* means "no quality set" (absent) versus 1,000,000,000 (parity rate
* when explicitly set). Keep this in mind when testing presence.
*
* @see SField, TypedField, SerializedTypeID
*/
#pragma once
#include <xrpl/basics/safe_cast.h>
@@ -9,15 +26,6 @@
namespace xrpl {
/*
Some fields have a different meaning for their
default value versus not present.
Example:
QualityIn on a TrustLine
*/
//------------------------------------------------------------------------------
// Forwards
@@ -35,6 +43,22 @@ class STVector256;
class STCurrency;
// NOLINTBEGIN(readability-identifier-naming)
/** Wire-type codes for XRPL binary serialization.
*
* Each value identifies the on-the-wire encoding family used for a group of
* protocol fields. Codes 111 ("common" types) fit in a single nibble and
* share a compact one-byte field-ID prefix. Codes 16+ ("uncommon" types)
* require an extra byte for the type nibble. Codes 1213 are reserved gaps.
* Codes 1000110004 are top-level container types (`STI_TRANSACTION`, etc.)
* that cannot be embedded inside other serialized objects.
*
* The enum and the companion string-to-int map `kS_TYPE_MAP` are both
* generated from a single `XMACRO` expansion — adding a new type requires
* only one line in the macro.
*
* @note These numeric values are protocol-stable: changing them would break
* binary serialization compatibility with existing ledger data and peers.
*/
#pragma push_macro("XMACRO")
#undef XMACRO
@@ -92,6 +116,12 @@ class STCurrency;
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum SerializedTypeID { XMACRO(TO_ENUM) };
/** String-to-integer map of all `SerializedTypeID` values.
*
* Generated by the same `XMACRO` expansion as `SerializedTypeID`, so the two
* are always in sync. Used to resolve type names arriving as text (e.g. from
* JSON or RPC) to their integer wire codes.
*/
static std::map<std::string, int> const kS_TYPE_MAP = {XMACRO(TO_MAP)};
#undef XMACRO
@@ -102,58 +132,127 @@ static std::map<std::string, int> const kS_TYPE_MAP = {XMACRO(TO_MAP)};
#pragma pop_macro("TO_MAP")
// NOLINTEND(readability-identifier-naming)
// constexpr
/** Pack a `SerializedTypeID` and per-type index into a single field code.
*
* The resulting integer is the canonical sort key used for deterministic
* binary serialization: the upper 16 bits hold the type family and the lower
* 16 bits hold the field's position within that family. Fields are always
* serialized in ascending `fieldCode` order.
*
* @param id The wire-type family (e.g. `STI_UINT32`).
* @param index The per-type field index (e.g. 4 for `sfSequence`).
* @return The packed field code `(id << 16) | index`.
*/
inline int
fieldCode(SerializedTypeID id, int index)
{
return (safeCast<int>(id) << 16) | index;
}
// constexpr
/** Pack a raw integer type ID and per-type index into a single field code.
*
* Overload for callers that already have the type as a plain `int` (e.g.
* when deserializing an unknown type from the wire).
*
* @param id The wire-type family as a raw integer.
* @param index The per-type field index.
* @return The packed field code `(id << 16) | index`.
*/
inline int
fieldCode(int id, int index)
{
return (id << 16) | index;
}
/** Identifies fields.
Fields are necessary to tag data in signed transactions so that
the binary format of the transaction can be canonicalized. All
SFields are created at compile time.
Each SField, once constructed, lives until program termination, and there
is only one instance per fieldType/fieldValue pair which serves the
entire application.
*/
/** Identifies a single named field in XRPL's binary serialization protocol.
*
* Every field that can appear in a transaction, ledger entry, validation, or
* transaction metadata is represented by exactly one `SField` singleton. All
* instances are created at static-initialization time in `SField.cpp` and
* live until program termination; copy, move, and assignment are deleted to
* enforce the singleton guarantee.
*
* Each field carries a packed `fieldCode` (`(SerializedTypeID << 16) |
* fieldValue`) that serves as both the registry key and the canonical
* comparison value for determining binary serialization order. Fields are
* always serialized in ascending `fieldCode` order — required for
* deterministic transaction signing.
*
* Construction is restricted to `SField.cpp` via `PrivateAccessTagT`: the
* tag type is forward-declared public here but defined only in that
* translation unit, so external code can only look up existing fields through
* `getField()`.
*
* @note Debug builds assert that no two fields share the same code or name at
* registration time. Release builds do not check; a duplicate would
* silently shadow the earlier field.
*
* @see TypedField, fieldCode(), SerializedTypeID
*/
class SField
{
public:
/** Never capture this field's value in transaction metadata. */
static constexpr auto kSMD_NEVER = 0x00;
static constexpr auto kSMD_CHANGE_ORIG = 0x01; // original value when it changes
static constexpr auto kSMD_CHANGE_NEW = 0x02; // new value when it changes
static constexpr auto kSMD_DELETE_FINAL = 0x04; // final value when it is deleted
static constexpr auto kSMD_CREATE = 0x08; // value when it's created
static constexpr auto kSMD_ALWAYS = 0x10; // value when node containing it is affected at all
static constexpr auto kSMD_BASE_TEN = 0x20; // value is treated as base 10, overriding behavior
static constexpr auto kSMD_PSEUDO_ACCOUNT = 0x40; // if this field is set in an ACCOUNT_ROOT
// _only_, then it is a pseudo-account
static constexpr auto kSMD_NEEDS_ASSET = 0x80; // This field needs to be associated with an
// asset before it is serialized as a ledger
// object. Intended for STNumber.
/** Capture the original value when the field changes. */
static constexpr auto kSMD_CHANGE_ORIG = 0x01;
/** Capture the new value when the field changes. */
static constexpr auto kSMD_CHANGE_NEW = 0x02;
/** Capture the final value when the enclosing object is deleted. */
static constexpr auto kSMD_DELETE_FINAL = 0x04;
/** Capture the value when the enclosing object is first created. */
static constexpr auto kSMD_CREATE = 0x08;
/** Capture the value whenever the enclosing ledger node is touched,
* regardless of whether the field itself changed (used by `sfRootIndex`). */
static constexpr auto kSMD_ALWAYS = 0x10;
/** Display the value in base-10 rather than hex in JSON metadata
* (used by MPT amount fields such as `sfMaximumAmount`). */
static constexpr auto kSMD_BASE_TEN = 0x20;
/** The field holds a 256-bit hash that identifies a pseudo-account
* (AMM, Vault, LoanBroker). Used by `sfAMMID`, `sfVaultID`,
* `sfLoanBrokerID`. */
static constexpr auto kSMD_PSEUDO_ACCOUNT = 0x40;
/** The field is an `STNumber` that must have `associateAsset()` called
* before the enclosing ledger object is serialized. The association
* rounds the value to the asset's precision and removes it if it becomes
* zero (pairs with `kSMD_DEFAULT`). */
static constexpr auto kSMD_NEEDS_ASSET = 0x80;
/** Default metadata flags: record original value, new value, deletion
* value, and creation value (`kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW |
* kSMD_DELETE_FINAL | kSMD_CREATE`). */
static constexpr auto kSMD_DEFAULT =
kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW | kSMD_DELETE_FINAL | kSMD_CREATE;
/** Controls whether a field is included in a transaction's signing payload.
*
* Fields that carry signatures (`sfTxnSignature`, `sfSigners`,
* `sfMasterSignature`, `sfSignature`, `sfCounterpartySignature`) are
* marked `No` to prevent the bootstrap paradox of a signature covering
* itself.
*/
enum class IsSigning : unsigned char { No, Yes };
/** Convenience constant for the non-signing value. */
static IsSigning const kNOT_SIGNING = IsSigning::No;
int const fieldCodeMem; // (type<<16)|index // TODO: rename, clashes with function
SerializedTypeID const fieldType; // STI_*
int const fieldValue; // Code number for protocol
/** Packed field code: `(SerializedTypeID << 16) | fieldValue`.
* This is the canonical sort key for binary serialization order.
* Sentinel values: -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`. */
int const fieldCodeMem;
/** Wire-type family for this field (e.g. `STI_UINT32`). */
SerializedTypeID const fieldType;
/** Per-type field index. Values < 256 are binary-serializable;
* values > 256 are JSON-only (discardable). */
int const fieldValue;
/** Human-readable field name without the `sf` prefix (e.g. `"Sequence"`). */
std::string const fieldName;
/** Bitmask of `kSMD_*` flags controlling transaction metadata capture. */
int const fieldMeta;
/** Monotonically increasing registration ordinal (1-based). */
int const fieldNum;
/** Whether this field is included in the signing payload. */
IsSigning const signingField;
/** JSON key for this field as a `StaticString` (pointer-stable). */
json::StaticString const jsonName;
SField(SField const&) = delete;
@@ -164,9 +263,29 @@ public:
operator=(SField&&) = delete;
public:
struct PrivateAccessTagT; // public, but still an implementation detail
/** Construction access guard — public type, private definition.
*
* Forward-declared here so the constructor signatures are visible, but
* the struct body (and its constructor) is defined only in `SField.cpp`.
* Consequently, only `SField.cpp` can construct `SField` instances.
*/
struct PrivateAccessTagT;
// These constructors can only be called from SField.cpp
/** Construct a typed, named protocol field and register it globally.
*
* Computes `fieldCode = (tid << 16) | fv` and inserts this field into
* the `knownCodeToField` and `knownNameToField` lookup tables. Only
* callable from `SField.cpp` (enforced by `PrivateAccessTagT`).
*
* @param tid Serialized type family (e.g. `STI_UINT32`).
* @param fv Per-type field index; must be < 256 to be
* binary-serializable.
* @param fn Human-readable field name (`sf` prefix already stripped
* by the calling macro).
* @param meta Bitmask of `kSMD_*` flags; defaults to `kSMD_DEFAULT`.
* @param signing Whether this field appears in signing payloads; defaults
* to `IsSigning::Yes`.
*/
SField(
PrivateAccessTagT,
SerializedTypeID tid,
@@ -174,118 +293,224 @@ public:
char const* fn,
int meta = kSMD_DEFAULT,
IsSigning signing = IsSigning::Yes);
/** Construct a special-purpose field from a raw field code.
*
* Used only for the four historical outlier fields (`kSF_INVALID`,
* `kSF_GENERIC`, `kSF_HASH`, `kSF_INDEX`) whose codes cannot be derived
* from the standard `(tid << 16) | fv` formula. Sets `fieldType` to
* `STI_UNKNOWN` and `fieldMeta` to `kSMD_NEVER`.
*
* @param fc Raw field code; -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`.
* @param fn Human-readable field name.
*/
explicit SField(PrivateAccessTagT, int fc, char const* fn);
/** Look up a registered field by its packed field code.
*
* @param fieldCode Packed code `(SerializedTypeID << 16) | fieldValue`.
* @return The matching `SField`, or `kSF_INVALID` if none is registered
* with that code.
*/
static SField const&
getField(int fieldCode);
/** Look up a registered field by its human-readable name.
*
* Names are stored without the `sf` prefix (e.g. `"Sequence"` not
* `"sfSequence"`).
*
* @param fieldName The name to search for (no `sf` prefix).
* @return The matching `SField`, or `kSF_INVALID` if none is registered
* with that name.
*/
static SField const&
getField(std::string const& fieldName);
/** Look up a registered field by raw integer type ID and field index.
*
* @param type Wire-type family as a raw integer.
* @param value Per-type field index.
* @return The matching `SField`, or `kSF_INVALID` if not found.
*/
static SField const&
getField(int type, int value)
{
return getField(fieldCode(type, value));
}
/** Look up a registered field by `SerializedTypeID` and field index.
*
* @param type Wire-type family.
* @param value Per-type field index.
* @return The matching `SField`, or `kSF_INVALID` if not found.
*/
static SField const&
getField(SerializedTypeID type, int value)
{
return getField(fieldCode(type, value));
}
/** Return the human-readable field name (without the `sf` prefix). */
[[nodiscard]] std::string const&
getName() const
{
return fieldName;
}
/** Return true if this field has a meaningful name and positive field code.
*
* Returns false for `kSF_INVALID` (`fieldCode == -1`) and `kSF_GENERIC`
* (`fieldCode == 0`).
*/
[[nodiscard]] bool
hasName() const
{
return fieldCodeMem > 0;
}
/** Return the JSON key for this field as a pointer-stable `StaticString`. */
[[nodiscard]] json::StaticString const&
getJsonName() const
{
return jsonName;
}
/** Implicit conversion to `json::StaticString` for use as a JSON key. */
operator json::StaticString const&() const
{
return jsonName;
}
/** Return true if this field is the `kSF_INVALID` sentinel (`fieldCode == -1`).
*
* `getField()` returns `kSF_INVALID` on a lookup miss.
*/
[[nodiscard]] bool
isInvalid() const
{
return fieldCodeMem == -1;
}
/** Return true if this field has a positive field code and can carry data.
*
* Equivalent to `!isInvalid() && hasName()`; false for `kSF_INVALID` and
* `kSF_GENERIC`.
*/
[[nodiscard]] bool
isUseful() const
{
return fieldCodeMem > 0;
}
/** Return true if this field can be round-tripped through binary serialization.
*
* A field is binary-serializable when `fieldValue < 256`. Fields with
* `fieldValue >= 256` (e.g. `kSF_HASH`, `kSF_INDEX`) exist only in JSON
* representations and are excluded from binary encoding.
*/
[[nodiscard]] bool
isBinary() const
{
return fieldValue < 256;
}
// A discardable field is one that cannot be serialized, and
// should be discarded during serialization,like 'hash'.
// You cannot serialize an object's hash inside that object,
// but you can have it in the JSON representation.
/** Return true if this field must be silently dropped during binary serialization.
*
* Discardable fields (e.g. `sfHash`, `sfIndex`) have `fieldValue > 256`
* and exist only in the JSON form of an object. A round-trip through
* binary will lose them.
*/
[[nodiscard]] bool
isDiscardable() const
{
return fieldValue > 256;
}
/** Return the packed field code `(SerializedTypeID << 16) | fieldValue`. */
[[nodiscard]] int
getCode() const
{
return fieldCodeMem;
}
/** Return the 1-based registration ordinal assigned at static-init time. */
[[nodiscard]] int
getNum() const
{
return fieldNum;
}
/** Return the total number of `SField` instances registered so far. */
static int
getNumFields()
{
return num;
}
/** Return true if any of the bits in `c` are set in this field's metadata mask.
*
* @param c A bitmask of one or more `kSMD_*` constants.
*/
[[nodiscard]] bool
shouldMeta(int c) const
{
return (fieldMeta & c) != 0;
}
/** Return true if this field should be included in a serialization pass.
*
* A field is included when it is binary-serializable (`fieldValue < 256`)
* and either the caller wants all fields (`withSigningField == true`) or
* this field is marked `IsSigning::Yes`. Passing `withSigningField ==
* false` excludes non-signing fields (used when building the signing
* payload for a transaction).
*
* @param withSigningField If false, fields marked `IsSigning::No` are
* excluded.
*/
[[nodiscard]] bool
shouldInclude(bool withSigningField) const
{
return (fieldValue < 256) && (withSigningField || (signingField == IsSigning::Yes));
}
/** Equality based on packed field code. */
bool
operator==(SField const& f) const
{
return fieldCodeMem == f.fieldCodeMem;
}
/** Inequality based on packed field code. */
bool
operator!=(SField const& f) const
{
return fieldCodeMem != f.fieldCodeMem;
}
/** Compare two fields by canonical binary-serialization order.
*
* Fields are ordered by `fieldCode = (SerializedTypeID << 16) |
* fieldValue`, sorting first by wire-type family and then by per-type
* index — matching the canonical XRPL binary format required for
* deterministic transaction signing.
*
* @param f1 First field.
* @param f2 Second field.
* @return -1 if `f1` precedes `f2`, 1 if `f1` follows `f2`, or 0 if
* the comparison is illegal because either field has a non-positive
* code (`kSF_INVALID` or `kSF_GENERIC`).
*/
static int
compare(SField const& f1, SField const& f2);
/** Return a read-only reference to the global code-to-field registry.
*
* The map key is the packed field code `(SerializedTypeID << 16) |
* fieldValue`. Intended for diagnostic and introspection use only;
* prefer `getField()` for ordinary lookups.
*/
static std::unordered_map<int, SField const*> const&
getKnownCodeToField()
{
@@ -298,7 +523,21 @@ private:
static std::unordered_map<std::string, SField const*> knownNameToField;
};
/** A field with a type known at compile time. */
/** An `SField` whose associated C++ type is known at compile time.
*
* Extends `SField` with a `type` alias so callers can statically verify that
* a field is read or written with the correct serialized C++ type. For
* example, `SF_UINT32` is `TypedField<STInteger<uint32_t>>`, making it a
* compile error to read it as an `STAmount`.
*
* All `TypedField` instances are singletons constructed in `SField.cpp`;
* external code cannot create new instances.
*
* @tparam T The serialized C++ type for this field (e.g. `STAmount`,
* `STInteger<uint32_t>`).
*
* @see OptionaledField, operator~
*/
template <class T>
struct TypedField : SField
{
@@ -308,7 +547,16 @@ struct TypedField : SField
explicit TypedField(PrivateAccessTagT pat, Args&&... args);
};
/** Indicate std::optional field semantics. */
/** Wrapper indicating that a `TypedField` may be absent in a given object.
*
* Obtained via `operator~(TypedField<T> const&)`. The `STObject` proxy
* access pattern uses this to return `std::optional<T>` instead of throwing
* when the field is missing.
*
* @tparam T The serialized C++ type of the underlying field.
*
* @see operator~
*/
template <class T>
struct OptionaledField
{
@@ -319,6 +567,15 @@ struct OptionaledField
}
};
/** Construct an `OptionaledField` from a `TypedField`, expressing optional semantics.
*
* Allows callers to write `~sfAmount` instead of `OptionaledField(sfAmount)`.
* The resulting value is used with the `STObject` proxy access API to obtain
* an `std::optional<T>` that is empty when the field is absent.
*
* @param f The typed field to treat as optional.
* @return An `OptionaledField<T>` wrapping `f`.
*/
template <class T>
inline OptionaledField<T>
operator~(TypedField<T> const& f)
@@ -328,13 +585,18 @@ operator~(TypedField<T> const& f)
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
using SF_UINT8 = TypedField<STInteger<std::uint8_t>>;
using SF_UINT16 = TypedField<STInteger<std::uint16_t>>;
using SF_UINT32 = TypedField<STInteger<std::uint32_t>>;
using SF_UINT64 = TypedField<STInteger<std::uint64_t>>;
using SF_UINT96 = TypedField<STBitString<96>>;
/** @defgroup SFieldTypeAliases Typed SField aliases
* Convenience type aliases pairing each `SerializedTypeID` wire family with
* its C++ serialized type. Use these as the type of `extern` field
* declarations so that the field carries full type information at compile
* time.
* @{
*/
using SF_UINT8 = TypedField<STInteger<std::uint8_t>>;
using SF_UINT16 = TypedField<STInteger<std::uint16_t>>;
using SF_UINT32 = TypedField<STInteger<std::uint32_t>>;
using SF_UINT64 = TypedField<STInteger<std::uint64_t>>;
using SF_UINT96 = TypedField<STBitString<96>>;
using SF_UINT128 = TypedField<STBitString<128>>;
using SF_UINT160 = TypedField<STBitString<160>>;
using SF_UINT192 = TypedField<STBitString<192>>;
@@ -342,17 +604,18 @@ using SF_UINT256 = TypedField<STBitString<256>>;
using SF_UINT384 = TypedField<STBitString<384>>;
using SF_UINT512 = TypedField<STBitString<512>>;
using SF_INT32 = TypedField<STInteger<std::int32_t>>;
using SF_INT64 = TypedField<STInteger<std::int64_t>>;
using SF_INT32 = TypedField<STInteger<std::int32_t>>;
using SF_INT64 = TypedField<STInteger<std::int64_t>>;
using SF_ACCOUNT = TypedField<STAccount>;
using SF_AMOUNT = TypedField<STAmount>;
using SF_ISSUE = TypedField<STIssue>;
using SF_CURRENCY = TypedField<STCurrency>;
using SF_NUMBER = TypedField<STNumber>;
using SF_VL = TypedField<STBlob>;
using SF_VECTOR256 = TypedField<STVector256>;
using SF_ACCOUNT = TypedField<STAccount>;
using SF_AMOUNT = TypedField<STAmount>;
using SF_ISSUE = TypedField<STIssue>;
using SF_CURRENCY = TypedField<STCurrency>;
using SF_NUMBER = TypedField<STNumber>;
using SF_VL = TypedField<STBlob>;
using SF_VECTOR256 = TypedField<STVector256>;
using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
/** @} */
//------------------------------------------------------------------------------
@@ -365,7 +628,19 @@ using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
#define UNTYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SField const sfName;
#define TYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SF_##stiSuffix const sfName;
/** Sentinel returned by `SField::getField()` on a lookup miss.
*
* `fieldCode == -1`; `isInvalid()` returns true. Callers that receive this
* value should treat the requested field as unrecognized.
*/
extern SField const kSF_INVALID;
/** Catch-all field for untyped serialization contexts.
*
* `fieldCode == 0`; `isUseful()` and `hasName()` return false. Used
* internally when a context requires an `SField` reference but no specific
* field is applicable.
*/
extern SField const kSF_GENERIC;
#include <xrpl/protocol/detail/sfields.macro>

View File

@@ -1,3 +1,13 @@
/** @file
* Schema definitions for XRPL serialized objects.
*
* Provides `SOElement` (a single field's schema entry) and `SOTemplate` (the
* complete ordered schema for one transaction, ledger entry, or inner object
* type). Templates are constructed once at startup by the `KnownFormats`
* singletons and are thereafter read-only, enabling lock-free O(1) field
* lookup during every serialization and deserialization call.
*/
#pragma once
#include <xrpl/basics/contract.h>
@@ -10,26 +20,70 @@
namespace xrpl {
/** Kind of element in each entry of an SOTemplate. */
/** Field-presence semantics for a single entry in an `SOTemplate`.
*
* Controls how `STObject` treats a field during deserialization, validation,
* and serialization:
*
* - `SoeRequired` — the field must be present; absence is a fatal error.
* - `SoeOptional` — the field may be absent; if present it may carry the
* type's default value (presence with default has distinct protocol meaning).
* - `SoeDefault` — the field may be absent; if present it must NOT carry the
* type's default value. Inner objects that contain `SoeDefault` fields must
* be created via `STObject::makeInnerObject()` to preserve this invariant.
* - `SoeInvalid` — sentinel returned by `STObject::getFieldStyle()` when the
* object has no associated template; never used in a live schema.
*
* @note `SoeOptional` and `SoeDefault` are subtly different: for some fields
* (e.g., `QualityIn` on a trust line) having the field present with its
* default value and having it absent carry different protocol semantics.
* Use `SoeDefault` when the field must not encode redundant default state.
*/
// 2026 usages, 129 files
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum SOEStyle {
SoeInvalid = -1,
SoeRequired = 0, // required
SoeOptional = 1, // optional, may be present with default value
SoeDefault = 2, // optional, if present, must not have default value
// inner object with the default fields has to be
// constructed with STObject::makeInnerObject()
SoeRequired = 0, ///< Field must be present.
SoeOptional = 1, ///< Field may be absent; if present, may hold default value.
SoeDefault = 2, ///< Field may be absent; if present, must not hold default value.
};
// Part of a Python-parsed DSL (transactions.macro); bare enumerator names required by the parser
/** Amount fields that can support MPT */
/** Multi-Purpose Token (MPT) awareness annotation for amount and issue fields.
*
* Applied only to `STAmount` and `STIssue` typed fields (enforced by the
* constrained `SOElement` constructor). Allows the validation layer in
* `STObject` and `STTx` to check MPT compatibility at the schema level rather
* than in scattered per-transaction code.
*
* - `SoeMptNone` — field does not carry an amount or issue; MPT check
* is never performed. Default for all non-amount fields.
* - `SoeMptSupported` — the transaction format allows MPT in this field.
* - `SoeMptNotSupported` — the transaction format explicitly forbids MPT in
* this field; validation rejects any MPT value.
*
* @note Bare enumerator names (without a class scope) are required because
* these values are parsed by the Python DSL that processes
* `transactions.macro`.
*/
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum SOETxMPTIssue { SoeMptNone, SoeMptSupported, SoeMptNotSupported };
//------------------------------------------------------------------------------
/** An element in a SOTemplate. */
/** One field's schema entry inside an `SOTemplate`.
*
* Pairs an `SField` reference with its `SOEStyle` presence semantics and,
* for amount/issue fields, an `SOETxMPTIssue` MPT-awareness tag.
*
* `SField` instances are immovable, non-copyable process-lifetime singletons.
* Storing a `std::reference_wrapper` rather than a raw pointer communicates
* the non-owning relationship clearly and allows `SOElement` to be held in a
* `std::vector` (which requires copyable/movable elements).
*
* @note Both constructors call the private `init()` helper, which throws if
* the field is not "useful" (i.e., `fieldCode <= 0`, as for `sfInvalid`
* or `sfGeneric`). This catches schema bugs at application startup.
*/
class SOElement
{
// Use std::reference_wrapper so SOElement can be stored in a std::vector.
@@ -38,6 +92,12 @@ class SOElement
SOETxMPTIssue supportMpt_ = SoeMptNone;
private:
/** Validate that the wrapped field is a known, named, serializable field.
*
* @param fieldName The field to validate.
* @throws std::runtime_error if `fieldName.isUseful()` returns false
* (i.e., `fieldCode <= 0`), indicating a sentinel or placeholder field.
*/
void
init(SField const& fieldName) const
{
@@ -51,11 +111,31 @@ private:
}
public:
/** Construct a schema entry for any serializable field.
*
* @param fieldName The field this entry describes; must satisfy
* `isUseful()` (positive field code).
* @param style Presence semantics: required, optional, or default.
* @throws std::runtime_error if @p fieldName is not a useful field.
*/
SOElement(SField const& fieldName, SOEStyle style) : sField_(fieldName), style_(style)
{
init(fieldName);
}
/** Construct a schema entry for an `STAmount` or `STIssue` field with MPT annotation.
*
* The `requires` constraint restricts this overload to `STAmount` and
* `STIssue` typed fields, enforcing that MPT support annotations can only
* appear on fields that actually carry amounts or asset specifiers.
*
* @tparam T Must be `STAmount` or `STIssue`.
* @param fieldName The typed amount or issue field this entry describes.
* @param style Presence semantics: required, optional, or default.
* @param supportMpt Whether this field accepts MPT values. Defaults to
* `SoeMptNotSupported` so new amount fields must explicitly opt in.
* @throws std::runtime_error if @p fieldName is not a useful field.
*/
template <typename T>
requires(std::is_same_v<T, STAmount> || std::is_same_v<T, STIssue>)
SOElement(
@@ -67,18 +147,26 @@ public:
init(fieldName);
}
/** Return the `SField` this entry describes. */
[[nodiscard]] SField const&
sField() const
{
return sField_.get();
}
/** Return the field's presence semantics within its containing object type. */
[[nodiscard]] SOEStyle
style() const
{
return style_;
}
/** Return the MPT-awareness annotation for this amount or issue field.
*
* @note Returns `SoeMptNone` for all non-amount, non-issue fields; callers
* should only interpret the result when the field type is `STAmount`
* or `STIssue`.
*/
[[nodiscard]] SOETxMPTIssue
supportMPT() const
{
@@ -88,67 +176,125 @@ public:
//------------------------------------------------------------------------------
/** Defines the fields and their attributes within a STObject.
Each subclass of SerializedObject will provide its own template
describing the available fields and their metadata attributes.
*/
/** Immutable field schema for one serialized object type in the XRP Ledger.
*
* Holds the ordered list of `SOElement` entries for a single transaction,
* ledger entry, or inner object type, together with a dense reverse-lookup
* table that maps `SField::getNum()` to the element's position in O(1).
*
* Templates are constructed once at process startup by `KnownFormats`
* subclasses (`TxFormats`, `LedgerFormats`, `InnerObjectFormats`) and are
* thereafter immutable. All consumers hold a `const*` or `const&`; no
* copying is ever required. Consequently the copy constructor and copy
* assignment operator are deleted — the type is move-only.
*
* @note The constructor snapshots `SField::getNumFields()` to size the index
* table. Fields registered after the template is constructed cannot be
* looked up and will cause `getIndex()` to throw. In practice this is
* never an issue because all `SField` singletons are registered before
* `main()` runs, ahead of the `KnownFormats` singletons.
*
* @see SOElement, SOEStyle, STObject::applyTemplate(), STObject::set()
*/
class SOTemplate
{
public:
SOTemplate(SOTemplate const&) = delete;
SOTemplate&
operator=(SOTemplate const&) = delete;
// Copying vectors is expensive. Make this a move-only type until
// there is motivation to change that.
SOTemplate(SOTemplate&& other) = default;
SOTemplate&
operator=(SOTemplate&& other) = default;
/** Create a template populated with all fields.
After creating the template fields cannot be added, modified, or removed.
*/
/** Build the schema from a type-specific and a shared field list.
*
* Concatenates @p uniqueFields followed by @p commonFields into a single
* ordered element sequence, then constructs the O(1) index table.
*
* @param uniqueFields Fields specific to this object type; placed first in
* the element sequence.
* @param commonFields Fields shared across all object types of this kind
* (e.g., `Fee`, `Sequence`, `SigningPubKey` for transactions); appended
* after unique fields.
* @throws std::runtime_error if any field has an out-of-range field number
* or appears more than once across both lists.
*/
SOTemplate(std::vector<SOElement> uniqueFields, std::vector<SOElement> commonFields = {});
/** Create a template populated with all fields.
Note: Defers to the vector constructor above.
*/
/** Convenience overload accepting initializer lists; delegates to the vector constructor.
*
* @param uniqueFields Fields specific to this object type.
* @param commonFields Fields shared across all object types of this kind.
* @throws std::runtime_error forwarded from the vector constructor.
*/
SOTemplate(
std::initializer_list<SOElement> uniqueFields,
std::initializer_list<SOElement> commonFields = {});
/* Provide for the enumeration of fields */
/** Return an iterator to the first `SOElement` in the schema. */
[[nodiscard]] std::vector<SOElement>::const_iterator
begin() const
{
return elements_.cbegin();
}
/** Return an iterator to the first `SOElement` in the schema. */
[[nodiscard]] std::vector<SOElement>::const_iterator
cbegin() const
{
return begin();
}
/** Return a past-the-end iterator for the element sequence. */
[[nodiscard]] std::vector<SOElement>::const_iterator
end() const
{
return elements_.cend();
}
/** Return a past-the-end iterator for the element sequence. */
[[nodiscard]] std::vector<SOElement>::const_iterator
cend() const
{
return end();
}
/** The number of entries in this template */
/** Return the number of field entries in this schema. */
[[nodiscard]] std::size_t
size() const
{
return elements_.size();
}
/** Retrieve the position of a named field. */
/** Return the position of @p sField in the element sequence, or -1 if absent.
*
* Uses a direct array subscript into the internal index table for O(1)
* cost. This is the hot path called on every field access during
* serialization and deserialization.
*
* @param sField The field to look up.
* @return Index into the element sequence, or -1 if the field is not part
* of this schema.
* @throws std::runtime_error if @p sField has a non-positive or
* out-of-range field number (i.e., a sentinel field or one registered
* after this template was constructed).
*/
[[nodiscard]] int
getIndex(SField const&) const;
/** Return the presence-style of @p sf within this schema.
*
* @param sf The field whose style to retrieve; must be present in this
* template (i.e., `getIndex(sf) != -1`).
* @return The `SOEStyle` declared for this field in the schema.
* @note Calling this with a field that is not in the template results in
* undefined behavior (out-of-bounds array access via the `-1` sentinel
* returned by `getIndex()`). Use `getIndex()` to check presence first
* when the field may be absent.
*/
[[nodiscard]] SOEStyle
style(SField const& sf) const
{
@@ -157,7 +303,7 @@ public:
private:
std::vector<SOElement> elements_;
std::vector<int> indices_; // field num -> index
std::vector<int> indices_; ///< Dense lookup table: field num -> index into elements_.
};
} // namespace xrpl

View File

@@ -1,5 +1,15 @@
#pragma once
/** @file
* Defines `STAccount`, the serialized-type wrapper for 160-bit XRPL account
* identifiers used inside transactions and ledger objects.
*
* The internal storage is a plain `AccountID` (`base_uint<160>`) — no heap
* allocation — while the wire format deliberately preserves the
* variable-length (VL) blob encoding of the original `STBlob`-based
* implementation for byte-for-byte ledger compatibility.
*/
#include <xrpl/basics/CountedObject.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STBase.h>
@@ -8,10 +18,28 @@
namespace xrpl {
/** Serialized-type wrapper for a 160-bit XRPL account identifier.
*
* `STAccount` stores an `AccountID` value in a fixed-size `uint160` (no
* heap allocation) while serializing and deserializing using the
* VL-prefixed blob encoding of the legacy `STBlob` implementation, keeping
* the wire format byte-for-byte compatible with all existing ledger data.
*
* A `bool default_` flag tracks whether the field has ever been explicitly
* assigned. A default field serializes as a zero-length VL blob, which is
* distinct from a field explicitly set to the all-zeros pseudo-account
* (`noAccount()`). Any call to `setValue()` or `operator=` clears the flag,
* even when the assigned value is zero.
*
* Inherits `CountedObject<STAccount>` for lock-free diagnostic instance
* counting, and is `final` — no further derivation is expected.
*
* @see STBase, CountedObject
*/
class STAccount final : public STBase, public CountedObject<STAccount>
{
private:
// The original implementation of STAccount kept the value in an STBlob.
// The original implementation kept the value in an STBlob.
// But an STAccount is always 160 bits, so we can store it with less
// overhead in an xrpl::uint160. However, so the serialized format of the
// STAccount stays unchanged, we serialize and deserialize like an STBlob.
@@ -21,40 +49,154 @@ private:
public:
using value_type = AccountID;
/** Construct an anonymous, unset account field.
*
* Sets the stored value to zero and marks the field as default (unset).
* A default field serializes as a zero-length VL blob and returns an
* empty string from `getText()`.
*/
STAccount();
/** Construct a named but unset account field.
*
* Binds the field to `n` but leaves it in the default (unset) state.
* Typical use: pre-populating an `STObject` slot before the account
* address is known.
*
* @param n The `SField` descriptor identifying this field (e.g. `sfAccount`).
*/
STAccount(SField const& n);
/** Construct from a raw VL-blob byte buffer.
*
* An empty buffer is the canonical round-trip encoding of a default
* (unset) field and leaves the object in the default state. A non-empty
* buffer must be exactly 20 bytes; any other size throws.
*
* @param n The `SField` descriptor for this field.
* @param v Raw bytes from a VL-blob read. Must be empty or exactly 20 bytes.
* @throws std::runtime_error if `v` is non-empty and not exactly 20 bytes.
*/
STAccount(SField const& n, Buffer const& v);
/** Deserialize an account field from a wire-format byte stream.
*
* Extracts the next VL-prefixed blob from `sit` and delegates to the
* `Buffer` constructor for size validation and value assignment.
*
* @param sit Forward cursor over the serialized byte stream; advanced
* past the VL blob on return.
* @param name The `SField` descriptor for this field.
* @throws std::runtime_error if the extracted blob is not empty or 20 bytes.
*/
STAccount(SerialIter& sit, SField const& name);
/** Construct from a known `AccountID` value.
*
* Marks the field as non-default regardless of whether `v` is the
* zero account. This is the standard path when the account address
* is already available at construction time.
*
* @param n The `SField` descriptor for this field.
* @param v The 160-bit account identifier to store.
*/
STAccount(SField const& n, AccountID const& v);
/** Return the `SerializedTypeID` constant for this type (`STI_ACCOUNT`). */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return the account address as a Base58Check string, or empty if unset.
*
* A default (unset) field returns `""` rather than the Base58 encoding
* of the all-zeros pseudo-account, preserving the distinction between an
* unset field and one explicitly set to `noAccount()`.
*
* @return Base58Check-encoded address, or `""` when `isDefault()` is true.
*/
[[nodiscard]] std::string
getText() const override;
/** Append this field to `s` using VL-blob wire encoding.
*
* A default (unset) field serializes as a zero-length VL blob (one
* `0x00` byte on the wire). A non-default field serializes as a 20-byte
* VL blob. This preserves byte-for-byte compatibility with the legacy
* `STBlob`-based encoding and distinguishes "unset" from "explicitly set
* to the zero account."
*
* @param s The `Serializer` to append to.
* @note Asserts (debug builds only) that the associated `SField` is a
* binary field of type `STI_ACCOUNT`.
*/
void
add(Serializer& s) const override;
/** Check semantic equivalence with another serialized field.
*
* Two `STAccount` objects are equivalent only when both their `default_`
* flags and their 160-bit values agree. The `SField` name is ignored —
* equivalence is purely about stored account state, not which field slot
* the object occupies.
*
* @param t The field to compare against.
* @return `true` if `t` is an `STAccount` with the same default flag and
* value; `false` if `t` is a different type or either attribute differs.
* @note Callers that need to compare only the address (ignoring default
* state) should use `operator==` on the `value()` accessors directly.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` if this field has never been explicitly assigned.
*
* A default field serializes as a zero-length VL blob. Assigning any
* `AccountID` — including the zero account — clears the default flag.
*/
[[nodiscard]] bool
isDefault() const override;
/** Assign an `AccountID` value, clearing the default flag.
*
* @param value The account identifier to store.
* @return `*this`, to support chained assignments.
*/
STAccount&
operator=(AccountID const& value);
/** Return the stored 160-bit account identifier.
*
* Returns the underlying `AccountID` regardless of whether the field is
* in the default state. Callers that need to distinguish "unset" from
* a real zero account should check `isDefault()` first.
*/
[[nodiscard]] AccountID const&
value() const noexcept;
/** Store `v` and mark this field as explicitly set.
*
* Unconditionally clears the default flag, even when `v` is the zero
* account, so that `isDefault()` returns `false` after any call.
*
* @param v The 160-bit account identifier to store.
*/
void
setValue(AccountID const& v);
private:
/** Place a copy of this object into `buf` (if it fits within `n` bytes)
* or heap-allocate a copy via `STBase::emplace()`.
*
* Used by `detail::STVar` for the small-object optimization.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Place a moved instance into `buf` (if it fits within `n` bytes)
* or heap-allocate via `STBase::emplace()`.
*
* Used by `detail::STVar` for the small-object optimization.
*/
STBase*
move(std::size_t n, void* buf) override;
@@ -81,30 +223,39 @@ STAccount::setValue(AccountID const& v)
default_ = false;
}
/** Return `true` if both `STAccount` objects hold the same 160-bit value.
*
* @note The default flag is not considered; use `isEquivalent()` when
* "set-ness" must also match.
*/
inline bool
operator==(STAccount const& lhs, STAccount const& rhs)
{
return lhs.value() == rhs.value();
}
/** Three-way-comparable less-than for two `STAccount` values. */
inline auto
operator<(STAccount const& lhs, STAccount const& rhs)
{
return lhs.value() < rhs.value();
}
/** Return `true` if the `STAccount` holds the same 160-bit value as `rhs`. */
inline bool
operator==(STAccount const& lhs, AccountID const& rhs)
{
return lhs.value() == rhs;
}
/** Less-than comparison between an `STAccount` and a raw `AccountID`. */
inline auto
operator<(STAccount const& lhs, AccountID const& rhs)
{
return lhs.value() < rhs;
}
/** Less-than comparison between a raw `AccountID` and an `STAccount`. */
inline auto
operator<(AccountID const& lhs, STAccount const& rhs)
{

View File

@@ -1,3 +1,12 @@
/** @file
* Canonical on-ledger amount type unifying XRP, IOU, and MPT quantities.
*
* `STAmount` is the serializable amount type used throughout the XRP Ledger.
* It stores XRP drops, IOU floating-point amounts, and Multi-Purpose Token
* (MPT) integers behind a single interface that integrates with the ledger's
* typed-field system via `STBase`.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -16,21 +25,47 @@
namespace xrpl {
// Internal form:
// 1: If amount is zero, then value is zero and offset is -100
// 2: Otherwise:
// legal offset range is -96 to +80 inclusive
// value range is 10^15 to (10^16 - 1) inclusive
// amount = value * [10 ^ offset]
// Wire form:
// High 8 bits are (offset+142), legal range is, 80 to 22 inclusive
// Low 56 bits are value, legal range is 10^15 to (10^16 - 1) inclusive
/** Unified serializable amount for XRP, IOU, and MPT assets.
*
* `STAmount` is the canonical on-ledger amount type. It stores three
* fundamentally different quantity kinds — XRP drops, IOU floating-point
* amounts, and Multi-Purpose Token integers — behind a single interface
* that integrates with the ledger's typed-field system via `STBase`.
*
* ## Internal representation
*
* For **IOU** amounts the value is stored as normalized scientific notation:
* `amount = value × 10^offset`. The mantissa is in `[kMIN_VALUE, kMAX_VALUE]`
* i.e. `[10^15, 10^16 1]`, and the exponent is in `[kMIN_OFFSET, kMAX_OFFSET]`
* i.e. `[-96, +80]`. Zero is encoded as `value = 0, offset = 100`; the
* sentinel 100 ensures that zero sorts below every positive IOU with a
* large-negative exponent.
*
* For **XRP and MPT** (`integral()` types) `offset` is always 0 and `value`
* directly holds the raw drop or token count. XRP is bounded by `kMAX_NATIVE_N`
* (10^17 drops); MPT is bounded by `INT64_MAX`.
*
* ## Wire encoding
*
* Amounts are serialised into a packed 64-bit word:
* - Bit 63 = 0 → native (XRP or MPT); bit 61 further distinguishes them.
* - Bit 63 = 1 → issued currency (IOU).
* - Bit 62 = sign (1 = positive).
* - For IOU: bits 5562 = `offset + 97`; bits 053 = mantissa.
*
* @note `canonicalize()` normalises the mantissa into `[kMIN_VALUE, kMAX_VALUE]`
* on every checked construction path. Constructors tagged `Unchecked` skip
* this step and require the caller to guarantee the representation is
* already canonical.
*/
class STAmount final : public STBase, public CountedObject<STAmount>
{
public:
/** Unsigned integer type used to store the IOU mantissa or integral amount value. */
using mantissa_type = std::uint64_t;
/** Signed integer type used to store the IOU base-10 exponent. */
using exponent_type = int;
/** Pair of (mantissa, exponent) for use in serialization and arithmetic helpers. */
using rep = std::pair<mantissa_type, exponent_type>;
private:
@@ -42,34 +77,82 @@ private:
public:
using value_type = STAmount;
/** Minimum legal IOU exponent (offset). Zero and integral types always use 0. */
constexpr static int kMIN_OFFSET = -96;
/** Maximum legal IOU exponent (offset). */
constexpr static int kMAX_OFFSET = 80;
// Maximum native value supported by the code
/** Minimum normalized IOU mantissa (10^15). Mantissas below this are scaled up. */
constexpr static std::uint64_t kMIN_VALUE = 1'000'000'000'000'000ull;
static_assert(isPowerOfTen(kMIN_VALUE));
/** Maximum normalized IOU mantissa (10^16 1). Mantissas above this are scaled down. */
constexpr static std::uint64_t kMAX_VALUE = (kMIN_VALUE * 10) - 1;
static_assert(kMAX_VALUE == 9'999'999'999'999'999ull);
/** Absolute maximum XRP/MPT value that the code will store internally
* (9 × 10^18 drops). Enforcement happens in the wire decoder and
* network-validity check (@ref isLegalNet). */
constexpr static std::uint64_t kMAX_NATIVE = 9'000'000'000'000'000'000ull;
// Max native value on network.
/** Maximum XRP drop value permitted on the network (10^17 = 100 billion XRP).
* Validated by @ref isLegalNet; amounts above this are consensus-invalid. */
constexpr static std::uint64_t kMAX_NATIVE_N = 100'000'000'000'000'000ull;
// --- Wire-format flag bits (bit 63 is MSB) ---
/** Wire bit 63: set for IOU amounts, clear for native (XRP or MPT). */
constexpr static std::uint64_t kISSUED_CURRENCY = 0x8'000'000'000'000'000ull;
/** Wire bit 62: sign bit — set means positive. */
constexpr static std::uint64_t kPOSITIVE = 0x4'000'000'000'000'000ull;
/** Wire bit 61: distinguishes MPT (set) from XRP (clear) for native amounts. */
constexpr static std::uint64_t kMP_TOKEN = 0x2'000'000'000'000'000ull;
/** Mask that strips the `kPOSITIVE` and `kMP_TOKEN` flag bits, leaving the
* raw value word for MPT amounts. */
constexpr static std::uint64_t kVALUE_MASK = ~(kPOSITIVE | kMP_TOKEN);
/** Wire encoding of a unit quality offer (rate = 1.0). */
static std::uint64_t const kU_RATE_ONE;
//--------------------------------------------------------------------------
//
// Constructors
//
//--------------------------------------------------------------------------
/** Deserialize an STAmount from a byte stream.
*
* Decodes the compact 64-bit wire word plus any trailing currency/issuer
* or MPTID bytes. Throws `std::runtime_error` on malformed input
* (negative zero, mantissa out of range, invalid currency/account).
*
* @param sit Source iterator positioned at the first byte of the amount.
* @param name The SField that names this field in the parent STObject.
*/
STAmount(SerialIter& sit, SField const& name);
/** Tag type that bypasses `canonicalize()` on construction.
*
* Use only when the caller can guarantee the representation is already
* in canonical form (e.g. inside arithmetic helpers that maintain
* invariants, or when reading from a known-good source). Prefer the
* checked constructors for all other call sites.
*/
struct Unchecked
{
explicit Unchecked() = default;
};
// Do not call canonicalize
/** Construct a named STAmount with a pre-canonical representation.
*
* Stores `mantissa × 10^exponent` (with sign) verbatim — `canonicalize()`
* is **not** called. The caller must ensure the values satisfy the IOU
* invariants or, for integral assets, that `exponent == 0`.
*
* @param name SField associated with this amount.
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Raw unsigned mantissa.
* @param exponent Base-10 exponent.
* @param negative True if the amount is negative.
*/
template <AssetType A>
STAmount(
SField const& name,
@@ -79,6 +162,15 @@ public:
bool negative,
Unchecked);
/** Construct an anonymous STAmount with a pre-canonical representation.
*
* Anonymous (no SField) variant of the `Unchecked` constructor above.
*
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Raw unsigned mantissa.
* @param exponent Base-10 exponent.
* @param negative True if the amount is negative.
*/
template <AssetType A>
STAmount(
A const& asset,
@@ -87,7 +179,18 @@ public:
bool negative,
Unchecked);
// Call canonicalize
/** Construct a named STAmount, calling `canonicalize()` afterward.
*
* Normalises the mantissa into `[kMIN_VALUE, kMAX_VALUE]` by adjusting
* the exponent. Throws `std::runtime_error` on overflow. Subnormals
* (exponent below `kMIN_OFFSET` after scaling) are silently zeroed.
*
* @param name SField associated with this amount.
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Unsigned mantissa (defaults to 0 → zero amount).
* @param exponent Base-10 exponent (defaults to 0).
* @param negative True if the amount is negative (defaults to false).
*/
template <AssetType A>
STAmount(
SField const& name,
@@ -96,14 +199,44 @@ public:
exponent_type exponent = 0,
bool negative = false);
/** Construct a named XRP amount from a signed 64-bit drop count.
*
* Negative values set the sign flag; the stored mantissa is the absolute value.
*
* @param name SField associated with this amount.
* @param mantissa Signed drop count.
*/
STAmount(SField const& name, std::int64_t mantissa);
/** Construct a named XRP amount from an unsigned 64-bit drop count.
*
* @param name SField associated with this amount.
* @param mantissa Unsigned drop count (defaults to 0).
* @param negative True if the amount is negative (defaults to false).
*/
STAmount(SField const& name, std::uint64_t mantissa = 0, bool negative = false);
/** Construct an anonymous XRP amount from an unsigned 64-bit drop count.
*
* @param mantissa Unsigned drop count (defaults to 0).
* @param negative True if the amount is negative (defaults to false).
*/
explicit STAmount(std::uint64_t mantissa = 0, bool negative = false);
/** Construct a named copy of an existing STAmount, preserving asset and value.
*
* @param name SField to attach to the copy.
* @param amt Source amount.
*/
explicit STAmount(SField const& name, STAmount const& amt);
/** Construct an anonymous STAmount with the given asset, calling `canonicalize()`.
*
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Unsigned mantissa (defaults to 0).
* @param exponent Base-10 exponent (defaults to 0).
* @param negative True if the amount is negative (defaults to false).
*/
template <AssetType A>
STAmount(A const& asset, std::uint64_t mantissa = 0, int exponent = 0, bool negative = false)
: asset_(asset), value_(mantissa), offset_(exponent), isNegative_(negative)
@@ -111,25 +244,84 @@ public:
canonicalize();
}
/** Construct an anonymous STAmount from a 32-bit unsigned mantissa.
*
* Widens to `uint64_t` then delegates to the canonical constructor.
*
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa 32-bit unsigned mantissa.
* @param exponent Base-10 exponent (defaults to 0).
* @param negative True if the amount is negative (defaults to false).
*/
// VFALCO Is this needed when we have the previous signature?
template <AssetType A>
STAmount(A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false);
/** Construct an anonymous STAmount from a signed 64-bit mantissa.
*
* Negative values set the sign flag; the stored mantissa is the absolute value.
*
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Signed mantissa; sign extracted via `set()`.
* @param exponent Base-10 exponent (defaults to 0).
*/
template <AssetType A>
STAmount(A const& asset, std::int64_t mantissa, int exponent = 0);
/** Construct an anonymous STAmount from a plain `int` mantissa.
*
* Widens to `int64_t` then delegates to the signed constructor.
*
* @param asset Asset type (Issue or MPTIssue).
* @param mantissa Signed integer mantissa.
* @param exponent Base-10 exponent (defaults to 0).
*/
template <AssetType A>
STAmount(A const& asset, int mantissa, int exponent = 0);
/** Construct an STAmount from a `Number`, rounding to the asset's precision.
*
* Converts the high-precision `Number` into the appropriate internal
* representation. For integral assets (XRP, MPT) the fractional part is
* dropped; for IOU assets the mantissa is normalised into
* `[kMIN_VALUE, kMAX_VALUE]`.
*
* @param asset Asset type (Issue or MPTIssue).
* @param number High-precision value to convert.
*/
template <AssetType A>
STAmount(A const& asset, Number const& number) : STAmount(fromNumber(asset, number))
{
}
// Legacy support for new-style amounts
/** Construct from a lean `IOUAmount` and its associated `Issue`.
*
* Bridges from the lightweight `IOUAmount` representation to the
* serializable `STAmount` form.
*
* @param amount Lean IOU amount (mantissa + exponent).
* @param issue Currency/issuer identity for the resulting STAmount.
*/
STAmount(IOUAmount const& amount, Issue const& issue);
/** Construct from a lean `XRPAmount`.
*
* @param amount XRP drop count.
*/
STAmount(XRPAmount const& amount);
/** Construct from a lean `MPTAmount` and its associated `MPTIssue`.
*
* @param amount Lean MPT amount (raw integer token count).
* @param mptIssue MPT issuance identity.
*/
STAmount(MPTAmount const& amount, MPTIssue const& mptIssue);
/** Convert to a high-precision `Number`.
*
* Dispatches via `Asset::visit()` to the appropriate lean extractor
* (`xrp()`, `iou()`, or `mpt()`) and constructs a `Number` from it.
*/
operator Number() const;
//--------------------------------------------------------------------------
@@ -138,39 +330,83 @@ public:
//
//--------------------------------------------------------------------------
/** Return the base-10 exponent.
*
* For IOU amounts this is in `[kMIN_OFFSET, kMAX_OFFSET]`, or 100 when
* the amount is zero. For XRP and MPT amounts this is always 0.
*/
[[nodiscard]] int
exponent() const noexcept;
/** True if this amount is an integral (non-floating-point) type.
*
* Returns true for both XRP and MPT; false for IOU. Integral types store
* `offset == 0` and a raw integer token count in `value`.
*/
[[nodiscard]] bool
integral() const noexcept;
/** True if this amount represents native XRP.
*
* Returns false for IOU and MPT amounts.
*/
[[nodiscard]] bool
native() const noexcept;
/** True if the embedded asset is of type `TIss`.
*
* @tparam TIss Either `Issue` (covers both XRP and IOU) or `MPTIssue`.
*/
template <ValidIssueType TIss>
[[nodiscard]] constexpr bool
holds() const noexcept;
/** True if this amount is negative.
*
* A canonical zero amount is never negative.
*/
[[nodiscard]] bool
negative() const noexcept;
/** Return the raw unsigned mantissa.
*
* For IOU amounts this is in `[kMIN_VALUE, kMAX_VALUE]` (or 0 for zero).
* For XRP and MPT amounts this is the raw drop or token count.
*/
[[nodiscard]] std::uint64_t
mantissa() const noexcept;
/** Return the asset (Issue or MPTIssue) carried by this amount. */
[[nodiscard]] Asset const&
asset() const;
/** Return the embedded asset as the specific issue type `TIss`.
*
* @tparam TIss Either `Issue` or `MPTIssue`.
* @throws std::logic_error if the asset is not of type `TIss`.
*/
template <ValidIssueType TIss>
constexpr TIss const&
get() const;
/** Mutable variant of `get<TIss>()`.
*
* @tparam TIss Either `Issue` or `MPTIssue`.
* @throws std::logic_error if the asset is not of type `TIss`.
*/
template <ValidIssueType TIss>
TIss&
get();
/** Return the issuer account for IOU amounts; `noAccount()` for XRP;
* the MPT issuer account for MPT amounts. */
[[nodiscard]] AccountID const&
getIssuer() const;
/** Return the sign as 1, 0, or +1.
*
* A canonical zero always returns 0 regardless of the `negative` flag.
*/
[[nodiscard]] int
signum() const noexcept;
@@ -178,9 +414,16 @@ public:
[[nodiscard]] STAmount
zeroed() const;
/** Populate a JSON object with the amount's fields (value, currency, issuer / mpt_issuance_id). */
void
setJson(json::Value&) const;
/** Returns a const reference to `*this`.
*
* Provided so that `STAmount` satisfies the same `value()` accessor
* pattern as the lean amount types (`XRPAmount`, `IOUAmount`, `MPTAmount`),
* enabling generic template code that calls `.value()` uniformly.
*/
[[nodiscard]] STAmount const&
value() const noexcept;
@@ -190,19 +433,34 @@ public:
//
//--------------------------------------------------------------------------
/** True if the amount is non-zero. */
explicit
operator bool() const noexcept;
/** Add `rhs` to this amount in place.
*
* @pre Both amounts must have the same asset; mixing asset types is
* undefined behaviour and will produce a wrong result at runtime.
*/
STAmount&
operator+=(STAmount const&);
/** Subtract `rhs` from this amount in place.
*
* @pre Both amounts must have the same asset; mixing asset types is
* undefined behaviour and will produce a wrong result at runtime.
*/
STAmount&
operator-=(STAmount const&);
/** Zero this amount, preserving its asset identity. */
STAmount& operator=(beast::Zero);
/** Assign from a lean `XRPAmount`, preserving the XRP asset identity. */
STAmount&
operator=(XRPAmount const& amount);
/** Assign from a `Number`, rounding to the current asset's precision. */
STAmount&
operator=(Number const&);
@@ -212,17 +470,32 @@ public:
//
//--------------------------------------------------------------------------
/** Flip the sign; a canonical zero amount is left unchanged. */
void
negate();
/** Reset to zero while keeping the current asset identity.
*
* For IOU amounts sets `offset` to 100 (the canonical zero sentinel so
* that zero sorts below small positive IOUs). For integral types sets
* `offset` to 0.
*/
void
clear();
// Zero while copying currency and issuer.
/** Reset to zero with a new asset identity.
*
* Equivalent to `setIssue(asset); clear();`.
*
* @param asset The asset to adopt.
*/
void
clear(Asset const& asset);
/** Set the Issue for this amount. */
/** Replace the asset identity without changing the value representation.
*
* @param asset New asset (Issue or MPTIssue).
*/
void
setIssue(Asset const& asset);
@@ -232,30 +505,68 @@ public:
//
//--------------------------------------------------------------------------
/** Returns `STI_AMOUNT`. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Returns a human-readable string including the field name and formatted value. */
[[nodiscard]] std::string
getFullText() const override;
/** Returns a formatted string representation of the numeric value. */
[[nodiscard]] std::string
getText() const override;
/** Serialize to JSON.
*
* XRP amounts are emitted as a plain decimal string (drop count).
* IOU amounts produce `{value, currency, issuer}`.
* MPT amounts produce `{value, mpt_issuance_id}`.
*/
[[nodiscard]] json::Value getJson(JsonOptions = JsonOptions::Values::None) const override;
/** Append the wire-format encoding to `s`.
*
* Writes the compact 64-bit word plus any trailing currency/issuer
* bytes (IOU) or 192-bit MPTID (MPT).
*/
void
add(Serializer& s) const override;
/** Returns true if `t` is an `STAmount` with the same asset and value.
*
* Comparison is performed on the binary representation, so canonical
* equivalence is checked, not numeric equality.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Returns true when the amount is zero.
*
* A field whose presence is governed by `soeDEFAULT` is omitted from
* ledger serialisation when `isDefault()` is true.
*/
[[nodiscard]] bool
isDefault() const override;
/** Extract the value as a lean `XRPAmount`.
*
* @throws std::logic_error if this is not a native XRP amount.
*/
[[nodiscard]] XRPAmount
xrp() const;
/** Extract the value as a lean `IOUAmount`.
*
* @throws std::logic_error if this is not an IOU amount.
*/
[[nodiscard]] IOUAmount
iou() const;
/** Extract the value as a lean `MPTAmount`.
*
* @throws std::logic_error if this is not an MPT amount.
*/
[[nodiscard]] MPTAmount
mpt() const;
@@ -354,7 +665,6 @@ STAmount::STAmount(A const& asset, int mantissa, int exponent)
{
}
// Legacy support for new-style amounts
inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue)
: asset_(issue), offset_(amount.exponent()), isNegative_(amount < beast::kZERO)
{
@@ -391,21 +701,70 @@ inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& mptIssue)
//
//------------------------------------------------------------------------------
// VFALCO TODO The parameter type should be Quality not uint64_t
/** Reconstruct an offer quality (rate) as a displayable STAmount.
*
* Decodes the packed `uint64_t` quality word produced by `getRate()` back
* into a human-readable IOU-denominated amount (no issuer).
*
* @param rate Encoded quality word (exponent in high byte, mantissa in low bits).
* @return An STAmount suitable for display or JSON output.
* @note The parameter type should eventually be `Quality` rather than `uint64_t`.
*/
STAmount
amountFromQuality(std::uint64_t rate);
/** Parse an amount from a decimal string for the given asset.
*
* Accepts a plain decimal string (possibly with an exponent suffix for IOU)
* or a drop-count string for XRP. Throws on malformed input.
*
* @param asset Target asset type.
* @param amount Decimal string representation.
* @return The parsed STAmount.
* @throws std::runtime_error on malformed input.
*/
STAmount
amountFromString(Asset const& asset, std::string const& amount);
/** Parse an STAmount from a JSON value, associating it with a named SField.
*
* Accepts three formats:
* - Plain string (XRP drop count).
* - `{value, currency, issuer}` object (IOU).
* - `{value, mpt_issuance_id}` object (MPT).
*
* Also accepts the legacy slash-delimited string format used in some RPC
* responses for historical compatibility.
*
* @param name SField to associate with the resulting STAmount.
* @param v JSON value to parse.
* @return The parsed STAmount.
* @throws std::runtime_error if the JSON is malformed or the values are out of range.
*/
STAmount
amountFromJson(SField const& name, json::Value const& v);
/** Non-throwing variant of `amountFromJson`.
*
* Parses a JSON value as an STAmount. On success writes to `result` and
* returns true; on any error leaves `result` unchanged and returns false.
*
* @param result Output parameter filled on success.
* @param jvSource JSON value to parse.
* @return True on success, false on any parse error.
*/
bool
amountFromJsonNoThrow(STAmount& result, json::Value const& jvSource);
// IOUAmount and XRPAmount define toSTAmount, defining this
// trivial conversion here makes writing generic code easier
/** Identity conversion so generic code can call `toSTAmount()` uniformly.
*
* `IOUAmount` and `XRPAmount` provide their own `toSTAmount()` overloads.
* This overload completes the set so that templates need not special-case
* `STAmount`.
*
* @param a The STAmount to pass through.
* @return A const reference to `a`.
*/
inline STAmount const&
toSTAmount(STAmount const& a)
{
@@ -555,8 +914,6 @@ STAmount::negate()
inline void
STAmount::clear()
{
// The -100 is used to allow 0 to sort less than a small positive values
// which have a negative exponent.
offset_ = integral() ? 0 : -100;
value_ = 0;
isNegative_ = false;
@@ -575,6 +932,14 @@ STAmount::value() const noexcept
return *this;
}
/** Returns true if the amount is a legal network value.
*
* For non-native amounts this is always true. For XRP amounts, the mantissa
* must not exceed `STAmount::kMAX_NATIVE_N` (10^17 drops = 100 billion XRP).
* Amounts that fail this check must not be included in consensus transactions.
*
* @param value The amount to test.
*/
inline bool
isLegalNet(STAmount const& value)
{
@@ -587,35 +952,55 @@ isLegalNet(STAmount const& value)
//
//------------------------------------------------------------------------------
/** Compare two STAmounts for equality.
*
* Two amounts are equal when they have identical asset, mantissa, exponent,
* and sign. Amounts of different asset types are never equal.
*/
bool
operator==(STAmount const& lhs, STAmount const& rhs);
/** Less-than comparison for STAmount.
*
* Defines a total order within the same asset type. Amounts of different
* asset types compare by asset identity first (implementation-defined stable
* order) so that STAmount can be used in ordered containers.
*/
bool
operator<(STAmount const& lhs, STAmount const& rhs);
/** Returns `!(lhs == rhs)`. */
inline bool
operator!=(STAmount const& lhs, STAmount const& rhs)
{
return !(lhs == rhs);
}
/** Returns `rhs < lhs`. */
inline bool
operator>(STAmount const& lhs, STAmount const& rhs)
{
return rhs < lhs;
}
/** Returns `!(rhs < lhs)`. */
inline bool
operator<=(STAmount const& lhs, STAmount const& rhs)
{
return !(rhs < lhs);
}
/** Returns `!(lhs < rhs)`. */
inline bool
operator>=(STAmount const& lhs, STAmount const& rhs)
{
return !(lhs < rhs);
}
/** Return the arithmetic negation of `value`.
*
* A zero amount is returned unchanged (canonical zero has no sign).
*/
STAmount
operator-(STAmount const& value);
@@ -625,36 +1010,110 @@ operator-(STAmount const& value);
//
//------------------------------------------------------------------------------
/** Add two same-asset STAmounts.
*
* @pre `v1` and `v2` must have the same asset.
*/
STAmount
operator+(STAmount const& v1, STAmount const& v2);
/** Subtract two same-asset STAmounts.
*
* @pre `v1` and `v2` must have the same asset.
*/
STAmount
operator-(STAmount const& v1, STAmount const& v2);
/** Divide `v1` by `v2`, expressing the result in `asset`.
*
* Designed for cross-currency calculations where the result naturally belongs
* to a third asset (e.g. quality calculations). Uses the amendment-gated
* arithmetic path (`getSTNumberSwitchover()`) for precision.
*
* @param v1 Dividend.
* @param v2 Divisor (must be non-zero).
* @param asset Asset type for the result.
* @return Quotient expressed as an STAmount with `asset`.
*/
STAmount
divide(STAmount const& v1, STAmount const& v2, Asset const& asset);
/** Multiply `v1` by `v2`, expressing the result in `asset`.
*
* @param v1 First factor.
* @param v2 Second factor.
* @param asset Asset type for the result.
* @return Product expressed as an STAmount with `asset`.
*/
STAmount
multiply(STAmount const& v1, STAmount const& v2, Asset const& asset);
// multiply rounding result in specified direction
/** Multiply with legacy fixed-direction rounding.
*
* Uses the legacy rounding approach: rounds up when the fractional
* remainder is ≥ 0.1 of the smallest representable unit.
* Prefer `mulRoundStrict` for new code that needs accurate rounding.
*
* @param v1 First factor.
* @param v2 Second factor.
* @param asset Asset type for the result.
* @param roundUp True to round up, false to round down.
* @return Rounded product expressed as an STAmount with `asset`.
*/
STAmount
mulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
// multiply following the rounding directions more precisely.
/** Multiply following the thread-local `Number::rounding_mode` precisely.
*
* Respects the `NumberRoundModeGuard` rounding mode for accurate remainder
* tracking, rather than the fixed legacy approximation used by `mulRound`.
*
* @param v1 First factor.
* @param v2 Second factor.
* @param asset Asset type for the result.
* @param roundUp True to round up, false to round down.
* @return Rounded product expressed as an STAmount with `asset`.
*/
STAmount
mulRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
// divide rounding result in specified direction
/** Divide with legacy fixed-direction rounding.
*
* Uses the legacy rounding approach. Prefer `divRoundStrict` for new code.
*
* @param v1 Dividend.
* @param v2 Divisor (must be non-zero).
* @param asset Asset type for the result.
* @param roundUp True to round up, false to round down.
* @return Rounded quotient expressed as an STAmount with `asset`.
*/
STAmount
divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
// divide following the rounding directions more precisely.
/** Divide following the thread-local `Number::rounding_mode` precisely.
*
* @param v1 Dividend.
* @param v2 Divisor (must be non-zero).
* @param asset Asset type for the result.
* @param roundUp True to round up, false to round down.
* @return Rounded quotient expressed as an STAmount with `asset`.
*/
STAmount
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
// Someone is offering X for Y, what is the rate?
// Rate: smaller is better, the taker wants the most out: in/out
// VFALCO TODO Return a Quality object
/** Encode an offer quality (in/out ratio) as a compact `uint64_t`.
*
* The rate represents `offerIn / offerOut`. A **smaller** value is better
* for the taker (more output per unit input). The encoding packs the
* base-10 exponent in the high byte and the mantissa in the remaining bits,
* making the values directly comparable as integers — which is the sort
* order used for offer-book directories.
*
* @param offerOut Amount the offer gives out.
* @param offerIn Amount the offer takes in.
* @return Packed quality word, or 0 if the result underflows.
* @note The return type should eventually be `Quality`.
*/
std::uint64_t
getRate(STAmount const& offerOut, STAmount const& offerIn);
@@ -722,26 +1181,63 @@ roundToAsset(
//------------------------------------------------------------------------------
/** Returns true if `amount` represents native XRP.
*
* Convenience wrapper around `STAmount::native()` for use in generic code
* that checks the asset type before dispatching.
*/
inline bool
isXRP(STAmount const& amount)
{
return amount.native();
}
/** Pre-flight check: returns true if `amt1 + amt2` is representable.
*
* For XRP and MPT amounts this performs 64-bit overflow/underflow bounds
* tests without executing the addition.
*
* For IOU amounts a relative-precision metric is used: both operands are
* reconstructed after a round-trip through addition and the combined
* relative error must not exceed 10^-4. This guards against silently
* losing significant digits when the operands' exponents differ by more
* than 15 (the mantissa precision limit).
*
* @param amt1 First operand.
* @param amt2 Second operand.
* @return True if the addition can be performed safely; false if it would
* overflow or produce an unacceptably imprecise result.
*/
bool
canAdd(STAmount const& amt1, STAmount const& amt2);
/** Pre-flight check: returns true if `amt1 - amt2` is representable.
*
* Equivalent to `canAdd(amt1, -amt2)`. Performs 64-bit underflow/overflow
* bounds tests for XRP and MPT; uses the relative-precision metric for IOU.
*
* @param amt1 Minuend.
* @param amt2 Subtrahend.
* @return True if the subtraction can be performed safely.
*/
bool
canSubtract(STAmount const& amt1, STAmount const& amt2);
/** Get the scale of a Number for a given asset.
/** Return the STAmount exponent that would result from converting `number`
* to an STAmount for the given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount, which has different rules
* and mantissa ranges for determining the exponent than Number.
* "Scale" is the base-10 exponent after STAmount normalization, which
* differs from `Number::exponent()` because STAmount enforces a narrower
* mantissa range (`[kMIN_VALUE, kMAX_VALUE]`) and asset-specific rules
* (integral assets always have exponent 0). This function constructs a
* temporary STAmount purely to read back the normalized exponent.
*
* @param number The Number to get the scale of.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
* Used by `roundToAsset` to determine the precision boundary before
* shedding sub-precision dust via `roundToScale`.
*
* @param number The high-precision value to inspect.
* @param asset The asset that governs normalization rules.
* @return The base-10 exponent of the normalized STAmount.
*/
inline int
scale(Number const& number, Asset const& asset)
@@ -753,6 +1249,20 @@ scale(Number const& number, Asset const& asset)
//------------------------------------------------------------------------------
namespace json {
/** Extract an STAmount from a JSON object by SField name.
*
* Specialisation of `json::getOrThrow<T>` for `xrpl::STAmount`. Looks up
* the field by its JSON key name in `v`, then delegates to
* `xrpl::amountFromJson` for full parsing (handles XRP string, IOU object,
* and MPT object formats).
*
* @param v JSON object containing the field.
* @param field SField whose JSON name is used as the lookup key.
* @return Parsed STAmount.
* @throws JsonMissingKeyError if the key is absent in `v`.
* @throws std::runtime_error if the value cannot be parsed as an STAmount.
*/
template <>
inline xrpl::STAmount
getOrThrow(json::Value const& v, xrpl::SField const& field)

View File

@@ -5,6 +5,28 @@
namespace xrpl {
/** An ordered, variable-length sequence of `STObject` instances.
*
* `STArray` is the protocol's container for repeated structured sub-fields
* within transactions and ledger entries — for example `sfMemos` (per-tx
* memo objects), `sfSigners` (multi-sign signer list), and `sfNFTokens` (NFT
* page entries). It participates fully in the XRPL binary wire format and
* the JSON/RPC layer.
*
* The binary format is sentinel-terminated: elements are encoded sequentially
* and the stream ends with an `(STI_ARRAY, 1)` marker rather than a length
* prefix. Each element must be an `STObject`; non-object field types and
* misplaced terminators throw `std::runtime_error` during deserialization.
*
* Instances are tracked by `CountedObject<STArray>` for diagnostic purposes
* (see `GetCounts`). The tracking cost is a single atomic
* increment/decrement per object lifetime.
*
* @note An empty `STArray` is considered the default value (`isDefault()`
* returns `true`), so enclosing `STObject` serializers will omit it from
* the wire encoding entirely — consistent with how absent optional array
* fields are represented in the ledger.
*/
class STArray final : public STBase, public CountedObject<STArray>
{
private:
@@ -21,12 +43,30 @@ public:
STArray() = default;
STArray(STArray const&) = default;
/** Construct an anonymous STArray from an iterator range of `STObject`s.
*
* The resulting array has no `SField` association. Use the two-argument
* overload when the array must be bound to a named field.
*
* @tparam Iter Forward iterator whose reference type is convertible to
* `STObject`.
* @param first Beginning of the source range.
* @param last One-past-the-end of the source range.
*/
template <
class Iter,
class = std::enable_if_t<
std::is_convertible_v<typename std::iterator_traits<Iter>::reference, STObject>>>
explicit STArray(Iter first, Iter last);
/** Construct an STArray bound to a field, initialized from an iterator range.
*
* @tparam Iter Forward iterator whose reference type is convertible to
* `STObject`.
* @param f The `SField` that names this array in its parent object.
* @param first Beginning of the source range.
* @param last One-past-the-end of the source range.
*/
template <
class Iter,
class = std::enable_if_t<
@@ -35,34 +75,131 @@ public:
STArray&
operator=(STArray const&) = default;
/** Move constructor.
*
* Explicitly copies the `SField` name from @p other before moving the
* element vector. `STBase` stores the field-name pointer separately from
* the data, so without this explicit transfer the moved-into object would
* carry a stale field association, causing field-ID mismatches during
* serialization.
*
* @param other The array to move from; left in a valid but unspecified state.
*/
STArray(STArray&&);
/** Move assignment operator.
*
* Same field-name transfer requirement as the move constructor.
*
* @param other The array to move from; left in a valid but unspecified state.
* @return `*this`
*/
STArray&
operator=(STArray&&);
/** Construct an STArray bound to a field with pre-allocated capacity.
*
* @param f The `SField` that names this array in its parent object.
* @param n Number of elements to reserve storage for.
*/
STArray(SField const& f, std::size_t n);
/** Deserializing constructor — decodes a sentinel-terminated sequence of
* inner objects from a binary stream.
*
* Loops over `(type, field)` pairs from @p sit until the canonical
* end-of-array marker (`STI_ARRAY, field == 1`) is encountered. Each
* iteration validates the next token and constructs an `STObject` element
* in place. After construction, `applyTemplateFromSField` validates the
* element against the registered schema for its field type (e.g. `sfMemo`,
* `sfSigner`).
*
* @param sit Forward cursor over the binary payload. Advanced in place.
* @param f The `SField` naming this array in its parent object.
* @param depth Current nesting depth threaded from the parent `STObject`.
* Incremented before each child `STObject` is constructed; `STObject`
* enforces a maximum depth of 10 to prevent stack exhaustion from
* crafted payloads.
* @throws std::runtime_error with message `"Illegal terminator in array"`
* if a misplaced end-of-object marker `(STI_OBJECT, 1)` is found.
* @throws std::runtime_error with message `"Unknown field"` if an
* unrecognized `(type, field)` pair is encountered.
* @throws std::runtime_error with message `"Non-object in array"` if a
* non-`STI_OBJECT` element type appears in the stream.
* @throws std::runtime_error if `applyTemplateFromSField` rejects an
* element; the partially constructed array is abandoned entirely.
*/
STArray(SerialIter& sit, SField const& f, int depth = 0);
/** Construct an anonymous STArray with pre-allocated capacity.
*
* Creates an array with no `SField` association but with storage reserved
* for @p n elements, avoiding early reallocations when the size is known
* up front.
*
* @param n Number of elements to reserve space for.
*/
explicit STArray(int n);
/** Construct an empty STArray bound to the given field.
*
* @param f The `SField` that names this array in its parent object.
*/
explicit STArray(SField const& f);
/** Access element at index @p j without bounds checking.
*
* @param j Zero-based index; behaviour is undefined if `j >= size()`.
* @return Reference to the element at position @p j.
*/
STObject&
operator[](std::size_t j);
/** Access element at index @p j without bounds checking (const overload).
*
* @param j Zero-based index; behaviour is undefined if `j >= size()`.
* @return Const reference to the element at position @p j.
*/
STObject const&
operator[](std::size_t j) const;
/** Access the last element without bounds checking.
*
* @return Reference to the last element; behaviour is undefined if the
* array is empty.
*/
STObject&
back();
/** Access the last element without bounds checking (const overload).
*
* @return Const reference to the last element; behaviour is undefined if
* the array is empty.
*/
[[nodiscard]] STObject const&
back() const;
/** Construct an `STObject` in place at the end of the array.
*
* @tparam Args Constructor argument types forwarded to `STObject`.
* @param args Arguments forwarded to the `STObject` constructor.
*/
template <class... Args>
void
emplaceBack(Args&&... args);
/** Append a copy of @p object to the end of the array.
*
* @param object The element to copy-append.
*/
void
pushBack(STObject const& object);
/** Append @p object to the end of the array by move.
*
* @param object The element to move-append.
*/
void
pushBack(STObject&& object);
@@ -81,72 +218,187 @@ public:
pushBack(std::move(object));
}
/** Return an iterator to the first element. */
iterator
begin();
/** Return an iterator to one past the last element. */
iterator
end();
/** Return a const iterator to the first element. */
[[nodiscard]] const_iterator
begin() const;
/** Return a const iterator to one past the last element. */
[[nodiscard]] const_iterator
end() const;
/** Return the number of elements in the array. */
[[nodiscard]] size_type
size() const;
/** Return `true` when the array contains no elements. */
[[nodiscard]] bool
empty() const;
/** Remove all elements, leaving an empty array. */
void
clear();
/** Reserve storage for at least @p n elements without changing the size.
*
* @param n Minimum capacity to reserve.
*/
void
reserve(std::size_t n);
/** Swap contents with @p a in constant time.
*
* @param a The other array to swap with.
*/
void
swap(STArray& a) noexcept;
/** Return a bracket-delimited, comma-separated string including field names.
*
* Each element is rendered via `STObject::getFullText()`. Intended for
* debugging and logging.
*
* @return Human-readable representation such as `[fieldA = ..., fieldB = ...]`.
*/
[[nodiscard]] std::string
getFullText() const override;
/** Return a bracket-delimited, comma-separated string of element values only.
*
* Each element is rendered via `STObject::getText()`, which omits field-name
* prefixes. Intended for debugging.
*
* @return Human-readable value-only representation of the array.
*/
[[nodiscard]] std::string
getText() const override;
/** Serialize this array to a JSON array value.
*
* Each element that is not `STI_NOTPRESENT` is appended as a JSON object
* with a single key — the element's field name — mapping to the element's
* own JSON representation:
* @code
* [ { "Memo": { "MemoData": "..." } }, ... ]
* @endcode
* Elements with type `STI_NOTPRESENT` (absent optional fields in a
* template-bound context) are silently skipped.
*
* @param index JSON rendering options forwarded to each element.
* @return A `json::Value` of array type.
*/
[[nodiscard]] json::Value
getJson(JsonOptions index) const override;
/** Append the binary encoding of every element to @p s.
*
* For each element, writes: field ID, element content, per-element object
* terminator (`STI_OBJECT, 1`). The outer array's own field ID and the
* end-of-array terminator (`STI_ARRAY, 1`) are written by the enclosing
* `STObject`, not here — each level is responsible only for its own body.
*
* @param s The serializer to append to.
*/
void
add(Serializer& s) const override;
/** Sort elements in place using a caller-supplied strict-weak-order comparator.
*
* Used to impose canonical ordering before serialization. Key callers:
* - `TxMeta::addRaw()` — sorts `AffectedNodes` by `sfLedgerIndex`; a
* deviation is a consensus-fork risk.
* - NFToken helpers — sorts `sfNFTokens` entries by `sfNFTokenID` when
* managing NFT pages.
*
* @note Multi-sign transactions require `sfSigners` to be sorted in
* ascending `AccountID` order before submission. That sort is expected
* to be performed by the signing client, not by the protocol layer.
*
* @param compare Function pointer returning `true` when the first argument
* should precede the second. Must satisfy strict-weak-ordering.
*/
void
sort(bool (*compare)(STObject const& o1, STObject const& o2));
/** Test element-wise equality with another `STArray`.
*
* @param s The array to compare against.
* @return `true` if both arrays have the same number of elements and each
* pair of corresponding elements compares equal via `STObject::operator==`.
*/
bool
operator==(STArray const& s) const;
/** Test element-wise inequality with another `STArray`.
*
* @param s The array to compare against.
* @return `true` if the arrays differ in size or any element pair is unequal.
*/
bool
operator!=(STArray const& s) const;
/** Erase the element at @p pos.
*
* @param pos Iterator to the element to remove.
* @return Iterator to the element following the erased one.
*/
iterator
erase(iterator pos);
/** Erase the element at @p pos (const_iterator overload).
*
* @param pos Const iterator to the element to remove.
* @return Iterator to the element following the erased one.
*/
iterator
erase(const_iterator pos);
/** Erase the elements in the range `[first, last)`.
*
* @param first Iterator to the first element to remove.
* @param last Iterator to one past the last element to remove.
* @return Iterator to the element following the last erased element.
*/
iterator
erase(iterator first, iterator last);
/** Erase the elements in the range `[first, last)` (const_iterator overload).
*
* @param first Const iterator to the first element to remove.
* @param last Const iterator to one past the last element to remove.
* @return Iterator to the element following the last erased element.
*/
iterator
erase(const_iterator first, const_iterator last);
/** Return `STI_ARRAY` — the serialized type ID for this class. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Test deep equality with another `STBase`.
*
* Performs a `dynamic_cast` to confirm @p t is also an `STArray`, then
* delegates to vector equality, which cascades through `STObject::operator==`.
*
* @param t The object to compare against.
* @return `true` if @p t is an `STArray` whose elements are pairwise equal
* to this array's elements.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` when the array is empty.
*
* An empty `STArray` is the default value; the enclosing `STObject`
* serializer will omit a default-valued field from the wire encoding.
*/
[[nodiscard]] bool
isDefault() const override;

View File

@@ -12,34 +12,48 @@
namespace xrpl {
/// Note, should be treated as flags that can be | and &
/** Bitmask controlling how an ST type renders to JSON.
*
* Combines named flag bits defined in `Values`. Supports `|`, `&`, and `~`
* for composing and masking option sets. The complement operator `~` is
* bounded by `Values::All` so it never sets reserved future bits.
*
* @note Treat instances as flag sets — bitwise operators are the intended
* interface; do not compare or store the raw `value` field directly.
*/
struct JsonOptions
{
using underlying_t = unsigned int;
underlying_t value;
/** Named option bits for JSON rendering. */
enum class Values : underlying_t {
None = 0b0000'0000,
IncludeDate = 0b0000'0001,
DisableApiPriorV2 = 0b0000'0010,
IncludeDate = 0b0000'0001, /**< Include a date field in the output. */
DisableApiPriorV2 = 0b0000'0010, /**< Suppress legacy pre-API-v2 formatting. */
// IMPORTANT `All` must be union of all of the above; see also operator~
All = IncludeDate | DisableApiPriorV2 // 0b0000'0011
};
/** Construct from a raw bitmask value. */
constexpr JsonOptions(underlying_t v) noexcept : value(v)
{
}
/** Construct from a named `Values` enumerator. */
constexpr JsonOptions(Values v) noexcept : value(static_cast<JsonOptions::underlying_t>(v))
{
}
/** Convert to the underlying unsigned integer. */
[[nodiscard]] constexpr explicit
operator underlying_t() const noexcept
{
return value;
}
/** Return `true` if any option bit is set. */
[[nodiscard]] constexpr explicit
operator bool() const noexcept
{
@@ -50,22 +64,26 @@ struct JsonOptions
[[nodiscard]] constexpr auto friend
operator!=(JsonOptions lh, JsonOptions rh) noexcept -> bool = default;
/// Returns JsonOptions union of lh and rh
/** Return the union (bitwise OR) of two option sets. */
[[nodiscard]] constexpr JsonOptions friend
operator|(JsonOptions lh, JsonOptions rh) noexcept
{
return {lh.value | rh.value};
}
/// Returns JsonOptions intersection of lh and rh
/** Return the intersection (bitwise AND) of two option sets. */
[[nodiscard]] constexpr JsonOptions friend
operator&(JsonOptions lh, JsonOptions rh) noexcept
{
return {lh.value & rh.value};
}
/// Returns JsonOptions binary negation, can be used with & (above) for set
/// difference e.g. `(options & ~JsonOptions::kINCLUDE_DATE)`
/** Return the complement, bounded to the known `Values::All` mask.
*
* Use with `&` for set-difference, e.g.
* `options & ~JsonOptions(JsonOptions::Values::IncludeDate)`.
* Bits beyond `Values::All` are never set in the result.
*/
[[nodiscard]] constexpr JsonOptions friend
operator~(JsonOptions v) noexcept
{
@@ -73,6 +91,17 @@ struct JsonOptions
}
};
/** ADL-accessible JSON conversion for any type that exposes `getJson(JsonOptions)`.
*
* Calls `t.getJson(JsonOptions::Values::None)`. Provides a uniform,
* options-free entry point for generic code that needs to render an ST value
* without caring about per-call rendering flags.
*
* @tparam T A type whose `getJson` method returns a value convertible to
* `json::Value`.
* @param t The object to convert.
* @return A `json::Value` representation of @p t.
*/
template <typename T>
requires requires(T const& t) {
{ t.getJson(JsonOptions::Values::None) } -> std::convertible_to<json::Value>;
@@ -100,85 +129,214 @@ class STVar;
//------------------------------------------------------------------------------
/** A type which can be exported to a well known binary format.
A STBase:
- Always a field
- Can always go inside an eligible enclosing STBase
(such as STArray)
- Has a field name
Like JSON, a SerializedObject is a basket which has rules
on what it can hold.
@note "ST" stands for "Serialized Type."
*/
/** Abstract base class for every serialized field type in the XRPL protocol.
*
* "ST" stands for "Serialized Type." Every field that appears in a
* transaction, ledger entry, or validation — integers, amounts, account IDs,
* blobs, arrays, nested objects — is represented as a class derived from
* `STBase`. Each instance carries a field identity (an `SField` pointer) that
* binds a human-readable name and a numeric type+field code used in the binary
* wire format.
*
* The virtual interface (`getSType`, `add`, `isEquivalent`, `isDefault`,
* `getJson`, `getText`, `getFullText`) must be overridden by every concrete
* subclass.
*
* @note `operator=` deliberately does **not** copy the field name when the
* destination already holds a meaningful name (see implementation). This
* design supports element slide-down in `STObject`/`STArray` without
* corrupting field identities. As a consequence, do **not** store
* `STBase`-derived objects directly in `std::vector` or similar owning
* containers — use `boost::ptr_*` containers or `detail::STVar` instead.
*/
class STBase
{
SField const* fName_;
public:
virtual ~STBase() = default;
/** Construct with the generic (placeholder) field name. */
STBase();
STBase(STBase const&) = default;
/** Copy-assign the value; conditionally copies the field name.
*
* The field name (`fName_`) is updated from @p t only when the current
* name is not useful (e.g., `sfGeneric`). This allows slot initialisation
* to pick up the source's protocol identity while preventing element
* slide-down operations inside `STObject` from overwriting already-valid
* field names.
*
* @param t The source object whose value (and optionally name) to copy.
* @return `*this`
*/
STBase&
operator=(STBase const& t);
/** Construct with a specific field identity.
*
* @param n The `SField` descriptor that identifies this field on the wire.
*/
explicit STBase(SField const& n);
/** Value equality: same concrete type and `isEquivalent` holds.
*
* Field names are ignored; only values are compared.
*/
bool
operator==(STBase const& t) const;
/** Value inequality: opposite of `operator==`. */
bool
operator!=(STBase const& t) const;
/** Narrow the static type to `D`, throwing on failure.
*
* Performs `dynamic_cast<D*>(this)`. Prefer this over a raw
* `dynamic_cast` at call sites — it guarantees that a failed cast throws
* `std::bad_cast` rather than yielding a null pointer that may be
* silently dereferenced.
*
* @tparam D The target derived type.
* @return A reference to `*this` as `D`.
* @throws std::bad_cast if `*this` is not an instance of `D`.
*/
template <class D>
D&
downcast();
/** Const overload of `downcast()`.
*
* @tparam D The target derived type.
* @return A const reference to `*this` as `D`.
* @throws std::bad_cast if `*this` is not an instance of `D`.
*/
template <class D>
D const&
downcast() const;
/** Return the `SerializedTypeID` enum value for this concrete type.
*
* The base implementation returns `STI_NOTPRESENT`. Every concrete
* subclass overrides this to return its own type tag.
*/
[[nodiscard]] virtual SerializedTypeID
getSType() const;
/** Return a human-readable string that includes the field name.
*
* Typically formatted as `"<fieldName> = <value>"`. Returns an empty
* string when `getSType() == STI_NOTPRESENT`.
*/
[[nodiscard]] virtual std::string
getFullText() const;
/** Return a human-readable string representation of the value only.
*
* Unlike `getFullText()`, the field name is not included. The base
* implementation returns an empty string.
*/
[[nodiscard]] virtual std::string
getText() const;
/** Render to a JSON value, respecting the given rendering options.
*
* @param options Bitmask controlling date inclusion and API version
* formatting. Defaults to `JsonOptions::Values::None`.
* @return A `json::Value` representation.
*/
[[nodiscard]] virtual json::Value getJson(JsonOptions = JsonOptions::Values::None) const;
/** Serialize the field's binary payload into @p s.
*
* Writes only the value bytes; the field-ID header must be written
* separately via `addFieldID()`. The base implementation is an
* unreachable stub — every concrete subclass must override this.
*
* @param s The `Serializer` accumulator to write into.
*/
virtual void
add(Serializer& s) const;
/** Value equivalence check, ignoring field names.
*
* Used by `operator==` and by `detail::STVar::operator==`. The base
* implementation asserts that this instance has type `STI_NOTPRESENT`
* and returns `true` only if @p t does as well. All concrete subclasses
* must override this.
*
* @param t The object to compare against.
* @return `true` if the two objects hold equivalent values.
*/
[[nodiscard]] virtual bool
isEquivalent(STBase const& t) const;
/** Return `true` if the field holds its default (zero-equivalent) value.
*
* Used during serialization to omit optional fields whose value is
* the type default. The base implementation always returns `true`.
*/
[[nodiscard]] virtual bool
isDefault() const;
/** A STBase is a field.
This sets the name.
*/
/** Set the field identity for this instance.
*
* @param n The `SField` descriptor to associate with this object.
*/
void
setFName(SField const& n);
/** Return the `SField` descriptor that identifies this field. */
[[nodiscard]] SField const&
getFName() const;
/** Write the type+field ID prefix bytes to @p s.
*
* Encodes the combined type code and field code as 13 bytes per the
* XRPL binary format, forming the header that precedes the value bytes
* written by `add()`. Asserts that the current field is a binary
* (wire-representable) field.
*
* @param s The `Serializer` to write the field ID into.
*/
void
addFieldID(Serializer& s) const;
protected:
/** Placement helper for the small-object optimization used by `detail::STVar`.
*
* If `sizeof(U) <= n`, constructs a `U` in @p buf via placement-new and
* returns a pointer to it. Otherwise heap-allocates a `U` with `new`.
* Concrete subclasses delegate to this from their `copy()` and `move()`
* overrides so that `detail::STVar` can store small types inline without
* a separate heap allocation.
*
* @tparam T The value type to construct (deduced); `U = std::decay_t<T>`.
* @param n Size of the inline buffer in bytes.
* @param buf Pointer to inline storage of at least @p n bytes.
* @param val The value to forward-construct into the buffer or heap.
* @return Pointer to the newly constructed `U` object.
*/
template <class T>
static STBase*
emplace(std::size_t n, void* buf, T&& val);
private:
/** Copy this object into @p buf (or heap) and return a pointer to it.
*
* Called exclusively by `detail::STVar` to implement copy construction.
* Delegates to `emplace(n, buf, *this)`.
*/
virtual STBase*
copy(std::size_t n, void* buf) const;
/** Move this object into @p buf (or heap) and return a pointer to it.
*
* Called exclusively by `detail::STVar` to implement move construction.
* Delegates to `emplace(n, buf, std::move(*this))`.
*/
virtual STBase*
move(std::size_t n, void* buf);
@@ -187,6 +345,14 @@ private:
//------------------------------------------------------------------------------
/** Stream an `STBase` as its full-text representation (field name and value).
*
* Equivalent to `out << t.getFullText()`.
*
* @param out The output stream to write to.
* @param t The serialized-type object to render.
* @return @p out, to allow chaining.
*/
std::ostream&
operator<<(std::ostream& out, STBase const& t);

View File

@@ -1,3 +1,10 @@
/** @file
* Defines `STBitString<Bits>`, the serialization-layer representation of
* fixed-width opaque bit arrays, and the four concrete aliases used
* throughout the protocol: `STUInt128`, `STUInt160`, `STUInt192`, and
* `STUInt256`.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -6,16 +13,34 @@
namespace xrpl {
// The template parameter could be an unsigned type, however there's a bug in
// gdb (last checked in gdb 12.1) that prevents gdb from finding the RTTI
// information of a template parameterized by an unsigned type. This RTTI
// information is needed to write gdb pretty printers.
/** Serialized fixed-width bit string field in the XRPL protocol type system.
*
* Bridges `BaseUInt<Bits>` — used for transaction hashes, account IDs,
* ledger indices, and similar opaque identifiers — and the `STBase`
* serialization framework. Despite the underlying type supporting
* arithmetic, this class treats its value as an opaque sequence of bits:
* only identity comparison, serialization, and value access are exposed.
*
* Each concrete instantiation (128, 160, 192, 256 bits) returns a
* distinct wire-type code from `getSType()` (`STI_UINT128` through
* `STI_UINT256`), so field metadata and codec behavior are type-correct at
* the protocol level.
*
* `CountedObject<STBitString<Bits>>` maintains a per-width live instance
* counter for diagnostic reporting; it carries no functional overhead.
*
* @tparam Bits Number of bits in the value. Declared `int` rather than
* `unsigned int` to work around a GDB 12.1 bug that prevents locating
* RTTI for templates instantiated over unsigned types; a `static_assert`
* enforces that the value is positive.
*/
template <int Bits>
class STBitString final : public STBase, public CountedObject<STBitString<Bits>>
{
static_assert(Bits > 0, "Number of bits must be positive");
public:
/** The underlying tag-free bit-string type (`BaseUInt<Bits>`). */
using value_type = BaseUInt<Bits>;
private:
@@ -24,47 +49,159 @@ private:
public:
STBitString() = default;
/** Construct a named field with a zero-initialized value.
*
* Used when building objects programmatically before the value is known.
*
* @param n The `SField` that identifies this field on the wire.
*/
STBitString(SField const& n);
/** Construct an anonymous value, discarding field identity.
*
* Intended for temporary computations where only the raw value matters.
*
* @param v The initial bit-string value.
*/
STBitString(value_type const& v);
/** Construct a fully specified named field with a given value.
*
* @param n The `SField` that identifies this field on the wire.
* @param v The initial bit-string value.
*/
STBitString(SField const& n, value_type const& v);
/** Deserialize a named field from a byte stream.
*
* Reads exactly `Bits/8` bytes from `sit` at the current cursor
* position via `SerialIter::getBitString<Bits>()`, centralizing
* deserialization logic in `SerialIter`.
*
* @param sit The input cursor; advanced by `Bits/8` bytes on success.
* @param name The `SField` that identifies this field on the wire.
* @throws std::runtime_error if the stream has fewer than `Bits/8`
* bytes remaining.
*/
STBitString(SerialIter& sit, SField const& name);
/** Return the wire-type identifier for this bit width.
*
* Specialized for each concrete alias: `STI_UINT128`, `STI_UINT160`,
* `STI_UINT192`, and `STI_UINT256`.
*
* @return The `SerializedTypeID` matching this instantiation's bit width.
*/
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return the hex-encoded string representation of the stored value.
*
* @return A lowercase hex string with no prefix.
*/
[[nodiscard]] std::string
getText() const override;
/** Test whether this field holds the same value as another `STBitString`.
*
* @param t The object to compare against.
* @return `true` if `t` is the same concrete bit width and both values
* are equal; `false` otherwise (including when `t` has a different
* bit width).
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Serialize the value into a `Serializer` byte buffer.
*
* Writes exactly `Bits/8` bytes to `s` via `Serializer::addBitString`.
*
* @param s The accumulator to write into.
* @pre `getFName().isBinary()` must be `true`.
* @pre `getFName().fieldType` must equal `getSType()`.
* @note Both preconditions are checked with `XRPL_ASSERT`; violations
* indicate a field/type metadata mismatch that would cause silent
* protocol corruption.
*/
void
add(Serializer& s) const override;
/** Return `true` when the stored value is the all-zeros bit string.
*
* The serialization layer uses this to decide whether a field may be
* omitted from canonical binary encoding.
*
* @return `true` if the value equals `beast::zero`.
*/
[[nodiscard]] bool
isDefault() const override;
/** Assign a new value, accepting any tag variant of `BaseUInt<Bits>`.
*
* The free `Tag` template parameter allows cross-tag assignment (e.g.,
* assigning a raw `uint256` to an `sfTransactionID` field) when the
* caller explicitly intends it, while making accidental mixing visible
* at the call site. Tag information is erased on storage.
*
* @tparam Tag The source tag type; any `BaseUInt<Bits, Tag>` is accepted.
* @param v The new value.
*/
template <typename Tag>
void
setValue(BaseUInt<Bits, Tag> const& v);
/** Return a const reference to the stored tag-free value.
*
* @return Reference to the internal `value_type`; valid for the lifetime
* of this object.
*/
[[nodiscard]] value_type const&
value() const;
/** Implicit conversion to the tag-free `value_type`.
*
* Erases any tag information from the underlying `BaseUInt` on the way
* out. Prefer `value()` in generic code to make the conversion explicit.
*/
operator value_type() const;
private:
/** Place-construct a copy into `buf` if it fits within `n` bytes;
* otherwise heap-allocate. Called by `detail::STVar` for the
* small-object optimisation inside `STObject` containers.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Place-construct a moved instance into `buf` if it fits within `n`
* bytes; otherwise heap-allocate. Called by `detail::STVar`.
*/
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
};
/** Serialized 128-bit opaque bit string (wire type `STI_UINT128`). */
using STUInt128 = STBitString<128>;
/** Serialized 160-bit opaque bit string (wire type `STI_UINT160`).
*
* Used for `AccountID` fields and similar 20-byte identifiers.
*/
using STUInt160 = STBitString<160>;
/** Serialized 192-bit opaque bit string (wire type `STI_UINT192`).
*
* Used for `MPTID` fields (32-bit sequence number ‖ 160-bit issuer).
*/
using STUInt192 = STBitString<192>;
/** Serialized 256-bit opaque bit string (wire type `STI_UINT256`).
*
* The most commonly used alias; carries transaction hashes, ledger
* indices, node IDs, and other 32-byte protocol identifiers.
*/
using STUInt256 = STBitString<256>;
template <int Bits>

View File

@@ -10,58 +10,180 @@
namespace xrpl {
// variable length byte string
/** Serialized-type representation of a variable-length binary field.
*
* `STBlob` backs any ledger or transaction field whose wire type is
* `STI_VL` (variable-length) **or** `STI_ACCOUNT` (20-byte account ID).
* Both types share identical VL-prefixed binary encoding on the wire;
* the semantic distinction is carried by the `SField` descriptor rather
* than this class.
*
* Storage is an owned `Buffer` (heap-backed `unique_ptr<uint8_t[]>`).
* Read access is always through a non-owning `Slice`, keeping the
* ownership model explicit: holding a `Slice` confers no ownership claim.
*
* @note Do not store `STBlob` (or any `STBase`-derived type) in
* `std::vector` or other standard containers — `STBase::operator=`
* intentionally does not copy field names, which breaks slide-down
* semantics. Use `detail::STVar` (via `STObject`) instead.
*/
class STBlob : public STBase, public CountedObject<STBlob>
{
Buffer value_;
public:
/** Non-owning view type used for all read access to the payload. */
using value_type = Slice;
STBlob() = default;
/** Copy-construct, duplicating the owned byte buffer. */
STBlob(STBlob const& rhs);
/** Construct by copying @p size bytes from @p data into an owned buffer.
*
* @param f The `SField` descriptor identifying this field.
* @param data Pointer to the source bytes (must not be null if
* @p size is non-zero).
* @param size Number of bytes to copy.
*/
STBlob(SField const& f, void const* data, std::size_t size);
STBlob(SField const& f, Buffer&& b);
STBlob(SField const& n);
STBlob(SerialIter&, SField const& name = kSF_GENERIC);
/** Construct by taking ownership of an existing buffer.
*
* @param f The `SField` descriptor identifying this field.
* @param b The buffer to move from; left empty after this call.
*/
STBlob(SField const& f, Buffer&& b);
/** Construct an empty (default) blob associated with field @p n.
*
* `isDefault()` returns `true` until the payload is set.
*
* @param n The `SField` descriptor identifying this field.
*/
STBlob(SField const& n);
/** Deserialize a variable-length blob from a byte stream.
*
* Reads the VL-prefixed byte sequence from @p st via
* `SerialIter::getVLBuffer()`.
*
* @param st Forward-only cursor over the serialized byte stream;
* advanced past the VL prefix and payload bytes on return.
* @param name The `SField` descriptor identifying this field.
*/
STBlob(SerialIter& st, SField const& name = kSF_GENERIC);
/** Return the number of bytes in the payload. */
[[nodiscard]] std::size_t
size() const;
/** Return a pointer to the first byte of the payload, or null if empty. */
[[nodiscard]] std::uint8_t const*
data() const;
/** Return `STI_VL`, the wire type tag for variable-length fields.
*
* @note Returns `STI_VL` even when the associated `SField` has type
* `STI_ACCOUNT`. Both types share identical VL-prefixed encoding;
* the field-ID byte written by the enclosing `STObject` carries the
* semantic distinction.
*/
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return the payload as an uppercase hex string for logging and JSON output. */
[[nodiscard]] std::string
getText() const override;
/** Serialize the payload into @p s with a VL length prefix.
*
* Writes the byte count followed by the raw bytes via
* `Serializer::addVL`, the exact inverse of the deserialization path.
*
* @param s The `Serializer` accumulator to append to.
* @note Asserts that the associated `SField` is a binary field and that
* its `fieldType` is `STI_VL` or `STI_ACCOUNT`. A field of any other
* type indicates a construction-time programming error and would
* produce a malformed wire encoding.
*/
void
add(Serializer& s) const override;
/** Return `true` if @p t is an `STBlob` with byte-identical content.
*
* Field names are not compared — only the raw payload bytes.
*
* @param t The other serialized-type object to compare against.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` if this blob holds no bytes (empty buffer).
*
* `STObject` uses this to omit optional fields whose payload has not
* been set, keeping wire representations compact.
*/
[[nodiscard]] bool
isDefault() const override;
/** Replace the payload with a copy of @p slice.
*
* Allocates a fresh `Buffer` and copies the bytes from @p slice.
* Use `operator=(Buffer&&)` or `setValue(Buffer&&)` to transfer
* ownership without a copy.
*
* @param slice Non-owning view of the source bytes.
* @return `*this`
*/
STBlob&
operator=(Slice const& slice);
/** Return a non-owning view of the payload.
*
* The returned `Slice` is valid only for the lifetime of this `STBlob`
* and is invalidated by any mutation (`operator=`, `setValue`).
*/
[[nodiscard]] value_type
value() const noexcept;
/** Transfer ownership of @p buffer to this blob in O(1).
*
* @p buffer is left empty after this call.
*
* @param buffer The buffer whose ownership is transferred.
* @return `*this`
*/
STBlob&
operator=(Buffer&& buffer);
/** Transfer ownership of @p b to this blob in O(1).
*
* Named alternative to `operator=(Buffer&&)` for call sites where an
* explicit setter reads more clearly than an assignment expression.
* @p b is left empty after this call.
*
* @param b The buffer whose ownership is transferred.
*/
void
setValue(Buffer&& b);
private:
/** Place-construct a copy into an `STVar` inline buffer or heap.
*
* Called exclusively by `detail::STVar`. Delegates to
* `STBase::emplace(n, buf, *this)`.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Place-construct a moved instance into an `STVar` inline buffer or heap.
*
* Called exclusively by `detail::STVar`. Delegates to
* `STBase::emplace(n, buf, std::move(*this))`, transferring `Buffer`
* ownership without copying the payload bytes.
*/
STBase*
move(std::size_t n, void* buf) override;

View File

@@ -1,3 +1,8 @@
/** @file
* Defines `STCurrency`, the serialized-type wrapper for 160-bit XRPL
* currency identifiers used inside transactions and ledger objects.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -8,6 +13,24 @@
namespace xrpl {
/** Serialized-type wrapper for a 160-bit XRPL currency identifier.
*
* `STCurrency` carries a `Currency` value (`base_uint<160,
* detail::CurrencyTag>`) inside the XRPL serialized-type field framework.
* Every field in a serialized transaction or ledger object must be an
* `STBase` subclass; `STCurrency` is the required adaptor for raw
* `Currency` values.
*
* The default (zero) value represents native XRP: `isDefault()` returns
* `true` whenever `isXRP(currency_)` is true, which causes the field to
* be omitted from canonical serialization when it carries no information
* beyond "this is XRP."
*
* Unlike `STAccount`, this class does not mix in `CountedObject`, so
* instance counts are not tracked for diagnostic purposes.
*
* @see STAccount, STIssue, Currency
*/
class STCurrency final : public STBase
{
private:
@@ -16,52 +39,164 @@ private:
public:
using value_type = Currency;
/** Construct an anonymous default (XRP) currency field. */
STCurrency() = default;
/** Deserialize a currency field from a binary wire stream.
*
* Reads exactly 160 bits from `sit` via `SerialIter::get160()`. No
* semantic validation is performed — binary data arriving from a
* consensus-validated ledger stream is assumed well-formed.
*
* @param sit Forward-only cursor over the serialized byte buffer;
* advanced by 20 bytes on return.
* @param name The `SField` descriptor for this field.
*/
explicit STCurrency(SerialIter& sit, SField const& name);
/** Construct a currency field with a known value.
*
* The standard programmatic constructor used when the `Currency` is
* already available (e.g., when building a transaction in memory).
*
* @param name The `SField` descriptor for this field.
* @param currency The 160-bit currency identifier to store.
*/
explicit STCurrency(SField const& name, Currency const& currency);
/** Construct a named but default (XRP) currency field.
*
* Binds the field to `name` and leaves the stored currency as the
* all-zeroes XRP value. Used when an `STObject` allocates a slot
* before the currency is known.
*
* @param name The `SField` descriptor for this field.
*/
explicit STCurrency(SField const& name);
/** Return the stored 160-bit currency identifier. */
[[nodiscard]] Currency const&
currency() const;
/** Return the stored 160-bit currency identifier.
*
* Alias for `currency()`, provided so generic code that expects a
* `value()` accessor on all ST wrapper types works uniformly.
*/
[[nodiscard]] Currency const&
value() const noexcept;
/** Replace the stored currency with `currency`.
*
* @param currency The new 160-bit currency identifier.
*/
void
setCurrency(Currency const& currency);
/** Return the `STI_CURRENCY` type tag used for field-dispatch and wire
* encoding.
*/
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return a human-readable currency string.
*
* Delegates to `to_string(currency_)`: returns `""` for XRP (zero),
* a three-character ISO-4217-style ticker (e.g., `"USD"`) for
* well-known tokens, or a hex string for opaque 160-bit custom
* currencies.
*
* @return String representation of the stored currency.
*/
[[nodiscard]] std::string
getText() const override;
/** Return a JSON string representation of the currency.
*
* The output is identical to `getText()`. The `JsonOptions` argument
* is ignored — a currency code is always a plain string with no
* optional decoration.
*
* @return JSON string value of the currency.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Append the currency to `s` as a raw 20-byte bit string.
*
* Writes the 160-bit value verbatim via `Serializer::addBitString`,
* with no VL prefix or other framing — consistent with all
* fixed-width scalar ST types.
*
* @param s The `Serializer` accumulator to append to.
*/
void
add(Serializer& s) const override;
/** Check semantic equivalence with another serialized field.
*
* Uses `dynamic_cast` to confirm `t` is also an `STCurrency`, then
* compares the stored 160-bit values. Returns `false` for any other
* `STBase` subtype.
*
* @param t The field to compare against.
* @return `true` if `t` is an `STCurrency` holding the same currency.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` when the stored currency is XRP (all-zeroes).
*
* In `STBase` semantics, fields at their default value are omitted
* from canonical serialization. An `STCurrency` whose value is XRP
* need not carry an explicit currency code on the wire.
*/
[[nodiscard]] bool
isDefault() const override;
private:
/** Factory called by `detail::STVar` when the wire type tag resolves
* to `STI_CURRENCY`. Delegates to the `SerialIter` constructor.
*/
static std::unique_ptr<STCurrency>
construct(SerialIter&, SField const& name);
/** Place a copy of this object into `buf` (if it fits within `n`
* bytes) or heap-allocate via `STBase::emplace()`.
* Used by `detail::STVar` for the small-object optimization.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Place a moved instance into `buf` (if it fits within `n` bytes)
* or heap-allocate via `STBase::emplace()`.
* Used by `detail::STVar` for the small-object optimization.
*/
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
};
/** Parse and validate a currency field from a JSON value.
*
* Acts as the defensive input validation gateway for API-sourced
* currency strings. Unlike binary deserialization, which trusts
* consensus-validated data, this function validates strictly because
* JSON input arrives from untrusted API consumers.
*
* Accepts only JSON string values. The string is converted via
* `toCurrency()` and then checked against two sentinel values that
* `toCurrency()` may silently return for invalid input:
* - `noCurrency()` — syntactically invalid string.
* - `badCurrency()` — the string `"XRP"` used as an IOU ticker, which
* is explicitly prohibited to prevent confusion with native XRP.
*
* @param name The `SField` descriptor for the resulting field.
* @param v The JSON value to parse; must be a string.
* @return An `STCurrency` holding the validated, non-reserved currency.
* @throws std::runtime_error if `v` is not a string, or if the string
* resolves to `badCurrency()` or `noCurrency()`.
*/
STCurrency
currencyFromJson(SField const& name, json::Value const& v);
@@ -83,30 +218,41 @@ STCurrency::setCurrency(Currency const& currency)
currency_ = currency;
}
/** Return `true` if both `STCurrency` objects hold the same 160-bit value. */
inline bool
operator==(STCurrency const& lhs, STCurrency const& rhs)
{
return lhs.currency() == rhs.currency();
}
/** Return `true` if the two `STCurrency` objects hold different values. */
inline bool
operator!=(STCurrency const& lhs, STCurrency const& rhs)
{
return !operator==(lhs, rhs);
}
/** Less-than comparison between two `STCurrency` values, enabling use in
* sorted containers.
*/
inline bool
operator<(STCurrency const& lhs, STCurrency const& rhs)
{
return lhs.currency() < rhs.currency();
}
/** Return `true` if the `STCurrency` holds the same 160-bit value as `rhs`.
*
* Avoids constructing a temporary `STCurrency` when comparing a wrapped
* field directly against an unwrapped `Currency` value.
*/
inline bool
operator==(STCurrency const& lhs, Currency const& rhs)
{
return lhs.currency() == rhs;
}
/** Less-than comparison between an `STCurrency` and a raw `Currency` value. */
inline bool
operator<(STCurrency const& lhs, Currency const& rhs)
{

View File

@@ -1,3 +1,15 @@
/** @file
* Type-safe bridge between serialized (`STBase`-derived) types and native C++
* types for reading and writing `STObject` fields.
*
* Application code works with plain C++ types (integers, `Slice`, `Buffer`),
* while the wire protocol stores everything in serialized form (`STInteger<U>`,
* `STBlob`, etc.). The `STExchange` traits struct centralizes the conversion
* mappings so callers never need to perform manual `dynamic_cast` or construct
* heap-allocated serialized objects directly. The free functions `get`, `set`,
* and `erase` are the primary interface for field access on `STObject`.
*/
#pragma once
#include <xrpl/basics/Blob.h>
@@ -17,23 +29,60 @@
namespace xrpl {
/** Convert between serialized type U and C++ type T. */
/** Traits adapter that maps a serialized type @p U to a native C++ type @p T.
*
* Each specialization provides:
* - `value_type` — the canonical C++ representation for the serialized type.
* - `get(optional<T>&, U const&)` — extracts a native value from the
* serialized object.
* - `set(field, T const&)` — constructs a heap-allocated serialized object
* ready for insertion into an `STObject`.
*
* All conversions are resolved at compile time; there is no runtime
* polymorphism in the type mapping itself. Adding support for a new C++ view
* of an existing wire type requires only a new specialization here — the
* serialization infrastructure is not touched.
*
* @tparam U The serialized type (e.g. `STInteger<uint32_t>`, `STBlob`).
* @tparam T The desired native C++ type (e.g. `uint32_t`, `Slice`, `Buffer`).
*/
template <class U, class T>
struct STExchange;
/** `STExchange` specialization covering the full family of integer types.
*
* A single partial specialization handles `STUInt8`, `STUInt16`, `STUInt32`,
* `STUInt64`, and `STInt32` uniformly. `get` extracts the integer via
* `STInteger::value()` and `set` constructs a new `STInteger<U>` on the heap.
*
* @tparam U The underlying integer type (e.g. `uint32_t`, `uint64_t`).
* @tparam T The desired C++ integer type to convert to/from.
*/
template <class U, class T>
struct STExchange<STInteger<U>, T>
{
explicit STExchange() = default;
/** The canonical C++ integer type for this serialized field. */
using value_type = U;
/** Populate @p t with the integer value stored in @p u.
*
* @param t Output optional to receive the extracted value.
* @param u The serialized integer object to read from.
*/
static void
get(std::optional<T>& t, STInteger<U> const& u)
{
t = u.value();
}
/** Construct a heap-allocated `STInteger<U>` initialized to @p t.
*
* @param f The field descriptor for the new serialized object.
* @param t The integer value to store.
* @return Owning pointer to the newly constructed serialized integer.
*/
static std::unique_ptr<STInteger<U>>
set(SField const& f, T const& t)
{
@@ -41,19 +90,37 @@ struct STExchange<STInteger<U>, T>
}
};
/** `STExchange` specialization for reading an `STBlob` field as a `Slice`.
*
* `Slice` is a non-owning view, so both `get` and `set` always copy the
* underlying bytes — `get` via `emplace(data, size)` and `set` via the
* `STBlob(field, data, size)` constructor.
*/
template <>
struct STExchange<STBlob, Slice>
{
explicit STExchange() = default;
/** Non-owning byte view. */
using value_type = Slice;
/** Populate @p t with a `Slice` pointing into a copy of @p u's bytes.
*
* @param t Output optional to receive the `Slice`.
* @param u The serialized blob object to read from.
*/
static void
get(std::optional<value_type>& t, STBlob const& u)
{
t.emplace(u.data(), u.size());
}
/** Construct a heap-allocated `STBlob` by copying the bytes of @p t.
*
* @param f The field descriptor for the new serialized object.
* @param t The source byte view to copy into the blob.
* @return Owning pointer to the newly constructed `STBlob`.
*/
static std::unique_ptr<STBlob>
set(TypedField<STBlob> const& f, Slice const& t)
{
@@ -61,25 +128,53 @@ struct STExchange<STBlob, Slice>
}
};
/** `STExchange` specialization for reading an `STBlob` field as a `Buffer`.
*
* `Buffer` owns its memory. The lvalue `set` overload copies bytes into the
* new `STBlob`; the rvalue `set` overload moves the `Buffer` directly into
* the `STBlob`, avoiding an extra heap allocation on hot paths that build
* transaction objects.
*/
template <>
struct STExchange<STBlob, Buffer>
{
explicit STExchange() = default;
/** Owning byte container. */
using value_type = Buffer;
/** Populate @p t with a `Buffer` containing a copy of @p u's bytes.
*
* @param t Output optional to receive the `Buffer`.
* @param u The serialized blob object to read from.
*/
static void
get(std::optional<Buffer>& t, STBlob const& u)
{
t.emplace(u.data(), u.size());
}
/** Construct a heap-allocated `STBlob` by copying the bytes of @p t.
*
* @param f The field descriptor for the new serialized object.
* @param t The source buffer to copy into the blob.
* @return Owning pointer to the newly constructed `STBlob`.
*/
static std::unique_ptr<STBlob>
set(TypedField<STBlob> const& f, Buffer const& t)
{
return std::make_unique<STBlob>(f, t.data(), t.size());
}
/** Construct a heap-allocated `STBlob` by moving @p t's storage.
*
* Preferred over the lvalue overload when the caller no longer needs
* the `Buffer`, as it avoids an extra heap allocation.
*
* @param f The field descriptor for the new serialized object.
* @param t The source buffer to move into the blob.
* @return Owning pointer to the newly constructed `STBlob`.
*/
static std::unique_ptr<STBlob>
set(TypedField<STBlob> const& f, Buffer&& t)
{
@@ -89,7 +184,26 @@ struct STExchange<STBlob, Buffer>
//------------------------------------------------------------------------------
/** Return the value of a field in an STObject as a given type. */
/** Read a field from an `STObject` as native C++ type @p T.
*
* Uses `STObject::peekAtPField` (non-mutating — does not insert a default for
* absent fields) and checks two distinct absence conditions: a null pointer
* (the field was never registered in the object's schema) and
* `STI_NOTPRESENT` (the field exists in the schema but has been explicitly
* marked absent). A `dynamic_cast` failure on the non-null, present field
* indicates a programming error and throws rather than returning empty.
*
* @tparam T The desired native C++ type to extract (e.g. `Slice` or `Buffer`
* for an `STBlob` field).
* @tparam U The serialized field type, inferred from @p f.
* @param st The object to read from.
* @param f The typed field descriptor identifying the field.
* @return The field value, or `std::nullopt` if the field is absent.
* @throws std::runtime_error If the field is present but its dynamic type
* does not match @p U — this indicates a programming error.
* @see get(STObject const&, TypedField<U> const&) for the type-inferring
* overload that avoids spelling out @p T explicitly.
*/
/** @{ */
template <class T, class U>
std::optional<T>
@@ -110,6 +224,19 @@ get(STObject const& st, TypedField<U> const& f)
return t;
}
/** Read a field from an `STObject`, inferring the native type from the field
* descriptor's `value_type`.
*
* This is the ergonomic default: callers write `get(st, sfSequence)` rather
* than `get<uint32_t>(st, sfSequence)`. Use the explicit-`T` overload when a
* different C++ view of the same wire type is needed (e.g. reading an `STBlob`
* as `Slice` for temporary inspection vs. `Buffer` for ownership).
*
* @tparam U The serialized field type, inferred from @p f.
* @param st The object to read from.
* @param f The typed field descriptor identifying the field.
* @return The field value as `U::value_type`, or `std::nullopt` if absent.
*/
template <class U>
std::optional<typename STExchange<U, typename U::value_type>::value_type>
get(STObject const& st, TypedField<U> const& f)
@@ -118,7 +245,21 @@ get(STObject const& st, TypedField<U> const& f)
}
/** @} */
/** Set a field value in an STObject. */
/** Write a value into a field of an `STObject`.
*
* Uses `std::decay` to strip cv-qualifiers and references before selecting the
* `STExchange` specialization, and `std::forward` to preserve value category
* so the move-semantic `Buffer&&` overload fires when an rvalue is passed.
*
* @tparam U The serialized field type, inferred from @p f.
* @tparam T The native C++ value type, inferred from @p t. May be an rvalue
* reference to trigger move-optimized specializations (e.g.
* `STExchange<STBlob, Buffer>::set(f, Buffer&&)`).
* @param st The object to write into.
* @param f The typed field descriptor identifying the field.
* @param t The value to store; forwarded to the appropriate `STExchange`
* specialization.
*/
template <class U, class T>
void
set(STObject& st, TypedField<U> const& f, T&& t)
@@ -126,7 +267,18 @@ set(STObject& st, TypedField<U> const& f, T&& t)
st.set(STExchange<U, std::decay_t<T>>::set(f, std::forward<T>(t)));
}
/** Set a blob field using an init function. */
/** Write a blob field whose contents are populated by a callable.
*
* Constructs an `STBlob` of @p size bytes and invokes @p init to fill it
* in-place, avoiding an intermediate copy for large blobs.
*
* @tparam Init A callable with signature compatible with the `STBlob`
* in-place initialization constructor.
* @param st The object to write into.
* @param f The field descriptor for the blob field.
* @param size The desired byte length of the blob.
* @param init Callable invoked to populate the blob's storage.
*/
template <class Init>
void
set(STObject& st, TypedField<STBlob> const& f, std::size_t size, Init&& init)
@@ -134,7 +286,16 @@ set(STObject& st, TypedField<STBlob> const& f, std::size_t size, Init&& init)
st.set(std::make_unique<STBlob>(f, size, init));
}
/** Set a blob field from data. */
/** Write a blob field from a raw pointer and length.
*
* Convenience overload for C-style interop. Copies @p size bytes from
* @p data into a newly constructed `STBlob`.
*
* @param st The object to write into.
* @param f The field descriptor for the blob field.
* @param data Pointer to the source bytes.
* @param size Number of bytes to copy.
*/
template <class = void>
void
set(STObject& st, TypedField<STBlob> const& f, void const* data, std::size_t size)
@@ -142,7 +303,17 @@ set(STObject& st, TypedField<STBlob> const& f, void const* data, std::size_t siz
st.set(std::make_unique<STBlob>(f, data, size));
}
/** Remove a field in an STObject. */
/** Mark a field as absent in an `STObject` without removing it from the schema.
*
* Delegates to `STObject::makeFieldAbsent`, which sets the field's type to
* `STI_NOTPRESENT`. The field slot is retained in the object's declared
* schema but contributes nothing to the wire encoding or canonical
* serialization.
*
* @tparam U The serialized field type, inferred from @p f.
* @param st The object to modify.
* @param f The typed field descriptor identifying the field to erase.
*/
template <class U>
void
erase(STObject& st, TypedField<U> const& f)

View File

@@ -5,62 +5,208 @@
namespace xrpl {
/** Wraps a plain integer inside the XRPL Serialized Type (ST) framework.
*
* Every integer-valued field in a ledger entry, transaction, or metadata
* object — sequence numbers, flags, fees, timestamps, transaction types —
* is represented at runtime as one of the five concrete aliases defined
* below (`STUInt8`, `STUInt16`, `STUInt32`, `STUInt64`, `STInt32`).
*
* The generic template supplies all methods that behave identically across
* integer widths (`add`, `isDefault`, `isEquivalent`, `operator=`,
* `copy`/`move` plumbing). Per-type specializations in `STInteger.cpp`
* provide `getSType()`, `getText()`, `getJson()`, and the deserialization
* constructor; these carry semantic knowledge that varies per instantiation
* (e.g., mapping `sfTransactionResult` bytes to TER strings, or rendering
* `STUInt64` as a JSON string to avoid IEEE 754 precision loss).
*
* `CountedObject<STInteger<Integer>>` adds a lock-free per-instantiation
* instance counter, so the diagnostic system can report live `STUInt32` and
* `STUInt64` counts separately.
*
* @tparam Integer The underlying C++ integer type (e.g., `std::uint32_t`).
*/
template <typename Integer>
class STInteger : public STBase, public CountedObject<STInteger<Integer>>
{
public:
/** The underlying integer type wrapped by this instantiation. */
using value_type = Integer;
private:
Integer value_;
public:
/** Construct an anonymous field holding @p v.
*
* The field has no associated `SField` name (uses the generic placeholder).
* Prefer the two-argument constructor when the field will be stored in an
* `STObject`, so the protocol field identity is preserved.
*
* @param v Initial value.
*/
explicit STInteger(Integer v);
/** Construct a named field holding @p v.
*
* @param n The `SField` descriptor that identifies this field on the wire.
* @param v Initial value; defaults to zero.
*/
STInteger(SField const& n, Integer v = 0);
/** Deserialize from a wire byte stream.
*
* Reads exactly `sizeof(Integer)` bytes from @p sit in big-endian order.
* Full specializations in `STInteger.cpp` provide the correct `sitGet*()`
* call for each instantiation width.
*
* @param sit Forward byte-stream cursor; advanced by `sizeof(Integer)`.
* @param name The `SField` descriptor to bind to the new object.
* @throws ripple::STObject::InvalidField or similar if @p sit underruns.
*/
STInteger(SerialIter& sit, SField const& name);
/** Return the `SerializedTypeID` tag for this integer width.
*
* Full specializations return `STI_UINT8`, `STI_UINT16`, `STI_UINT32`,
* `STI_UINT64`, or `STI_INT32` as appropriate for `Integer`.
*/
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Render the value to JSON, with field-identity-aware formatting.
*
* Most instantiations emit the raw integer. Notable exceptions:
* - `STUInt8` on `sfTransactionResult` → short TER token string (e.g.,
* `"tesSUCCESS"`); raw integer on unrecognized codes.
* - `STUInt16` on `sfLedgerEntryType`/`sfTransactionType` → registered
* format name string (e.g., `"AccountRoot"`, `"Payment"`).
* - `STUInt32` on `sfPermissionValue` → permission name string.
* - `STUInt64` → always a JSON *string* (never a number) to avoid IEEE 754
* precision loss; hex by default, decimal if `SField::sMD_BaseTen` is set.
*
* @param options Rendering flags (e.g., `JsonOptions::Values::None`).
* @return A `json::Value` representation of this field.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Return a human-readable string representation of the value.
*
* Applies the same field-identity-aware logic as `getJson()`, but returns
* a `std::string` suitable for diagnostics and logs rather than a JSON
* value. `STUInt8` on `sfTransactionResult` yields the long-form human
* description; `STUInt16` on `sfLedgerEntryType`/`sfTransactionType` yields
* the registered name; all others yield a decimal string.
*
* @return Human-readable string for the field's value.
*/
[[nodiscard]] std::string
getText() const override;
/** Serialize the integer value into @p s.
*
* Calls `s.addInteger(value_)`, which writes `sizeof(Integer)` bytes in
* big-endian order. Two `XRPL_ASSERT` guards fire in debug builds: one
* confirms the field is marked binary (`isBinary()`), and one confirms
* the field's declared type tag matches `getSType()`. A failing assert
* indicates a mis-wired field definition.
*
* @param s The `Serializer` accumulator to write into.
*/
void
add(Serializer& s) const override;
/** Return `true` if the wrapped value equals zero.
*
* The ST framework uses this to omit optional fields whose value is the
* type default, keeping wire representations canonical and compact.
*/
[[nodiscard]] bool
isDefault() const override;
/** Return `true` if @p t holds the same concrete type and the same value.
*
* Uses `dynamic_cast` to guard against comparing, say, an `STUInt32`
* with an `STUInt64` that happen to share the same bit pattern. Field
* names are ignored; only values are compared.
*
* @param t The object to compare against.
* @return `true` if @p t is the same `STInteger<Integer>` instantiation
* and holds the same wrapped value.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Assign a new raw value, preserving the field identity.
*
* @param v The new value to store.
* @return `*this`.
*/
STInteger&
operator=(value_type const& v);
/** Return the wrapped value without implicit conversion.
*
* Prefer this over `operator Integer()` when the intent is an explicit
* read; it is clearer at the call site that a raw integer is being
* extracted.
*/
[[nodiscard]] value_type
value() const noexcept;
/** Replace the wrapped value.
*
* @param v The new value to store.
*/
void
setValue(Integer v);
/** Implicit conversion to the underlying integer type.
*
* Allows `STInteger<T>` to be passed to functions expecting `T` without
* an explicit `.value()` call. Use `.value()` when clarity at the call
* site matters more than brevity.
*/
operator Integer() const;
private:
/** Copy this object into @p buf (or heap) via `STBase::emplace()`.
*
* Called exclusively by `detail::STVar` to implement copy construction
* with the small-object optimization.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Move this object into @p buf (or heap) via `STBase::emplace()`.
*
* Called exclusively by `detail::STVar` to implement move construction
* with the small-object optimization.
*/
STBase*
move(std::size_t n, void* buf) override;
friend class xrpl::detail::STVar;
};
/** 8-bit unsigned serialized integer; used for `sfTransactionResult`. */
using STUInt8 = STInteger<unsigned char>;
/** 16-bit unsigned serialized integer; used for `sfLedgerEntryType` and `sfTransactionType`. */
using STUInt16 = STInteger<std::uint16_t>;
/** 32-bit unsigned serialized integer; the most common integer field width. */
using STUInt32 = STInteger<std::uint32_t>;
/** 64-bit unsigned serialized integer.
*
* Always rendered as a JSON string (never a JSON number) to avoid IEEE 754
* precision loss. Fields annotated with `SField::sMD_BaseTen` render as
* decimal; all others render as lowercase hexadecimal.
*/
using STUInt64 = STInteger<std::uint64_t>;
/** 32-bit signed serialized integer. */
using STInt32 = STInteger<std::int32_t>;
template <typename Integer>

View File

@@ -8,6 +8,25 @@
namespace xrpl {
/** Serialized representation of a fungible asset identifier (XRP, IOU, or MPT).
*
* `STIssue` is the canonical `STBase` subtype for embedding an `Asset` inside
* a ledger object or transaction field. It bridges the polymorphic serialization
* framework and the `Asset` variant that unifies all three asset species.
*
* The wire format is type-multiplexed without a separate tag byte: XRP is
* a single 160-bit all-zeros currency sentinel; IOU is a 160-bit currency
* followed by a 160-bit issuer AccountID; MPT is a 160-bit issuer AccountID
* followed by the 160-bit `noAccount()` sentinel and a 32-bit sequence.
* The `noAccount()` sentinel is the discriminator between IOU and MPT — it is
* otherwise an illegal issuer address and will never appear in real IOU data.
*
* The class is declared `final`; the `STBase` hierarchy is complete without
* further inheritance. `CountedObject<STIssue>` instruments construction and
* destruction for runtime diagnostics.
*
* @see Asset, Issue, MPTIssue
*/
class STIssue final : public STBase, CountedObject<STIssue>
{
private:
@@ -19,56 +38,155 @@ public:
STIssue() = default;
STIssue(STIssue const& rhs) = default;
/** Deserialize an STIssue from a byte stream, detecting XRP, IOU, or MPT.
*
* Reads a 160-bit slot. If all-zeros (XRP currency sentinel), the asset is
* XRP and deserialization is complete. Otherwise reads a second 160-bit slot:
* if it equals `noAccount()`, the asset is an MPT — a 32-bit sequence number
* follows and the three values are assembled into an `MPTID`. Any other
* second slot forms the `(currency, account)` pair of an IOU `Issue`.
*
* @param sit Forward cursor over the serialized byte buffer; advanced in place.
* @param name The SField identifying this field within its parent STObject.
* @throws std::runtime_error if an IOU's currency/account native-flag
* combination is invalid (e.g., XRP currency paired with a non-XRP account).
*/
explicit STIssue(SerialIter& sit, SField const& name);
/** Construct an STIssue tagged to a specific field and holding the given asset.
*
* Accepts any type satisfying the `AssetType` concept (`Issue`, `MPTIssue`,
* `MPTID`, or `Asset`). For `Issue`-typed assets, the currency/account
* native-flag combination is validated via `isConsistent()`; MPT issuances
* are always considered consistent.
*
* @tparam A An `AssetType` — `Issue`, `MPTIssue`, `MPTID`, or `Asset`.
* @param name The SField that names this field within a parent STObject.
* @param issue The asset to wrap.
* @throws std::runtime_error if the asset is an `Issue` and its currency
* and account native flags are inconsistent.
*/
template <AssetType A>
explicit STIssue(SField const& name, A const& issue);
/** Construct an XRP STIssue tagged to a specific field.
*
* Convenience constructor producing the default (XRP) asset bound to
* the given field name. Equivalent to `STIssue(name, xrpIssue())`.
*
* @param name The SField identifying this field within its parent STObject.
*/
explicit STIssue(SField const& name);
STIssue&
operator=(STIssue const& rhs) = default;
/** Return the held asset as the concrete type `TIss`.
*
* @tparam TIss The requested type — `Issue` or `MPTIssue`.
* @return A const reference to the underlying `TIss` value.
* @throws std::runtime_error if the variant does not hold `TIss`.
*/
template <ValidIssueType TIss>
TIss const&
get() const;
/** Return whether the held asset is of type `TIss`.
*
* @tparam TIss The type to query — `Issue` or `MPTIssue`.
* @return `true` if the underlying `Asset` variant holds `TIss`.
*/
template <ValidIssueType TIss>
[[nodiscard]] bool
holds() const;
/** Return the underlying `Asset` without copying or throwing.
*
* @return A const reference to the wrapped `Asset`.
*/
[[nodiscard]] value_type const&
value() const noexcept;
/** Replace the held asset, re-running the consistency check for IOU issues.
*
* If the current asset is an `Issue`, `isConsistent()` is applied to the
* incoming value before assignment. MPT assets always pass through.
*
* @param issue The new asset to store.
* @throws std::runtime_error if `issue` is an `Issue` with mismatched
* native-flag and account.
*/
void
setIssue(Asset const& issue);
/** @return The serialized type identifier `STI_ISSUE` for generic field dispatch. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** @return A human-readable string for the asset (e.g., `"XRP"`,
* `"USD/r..."`, or the raw hex of an MPTID).
*/
[[nodiscard]] std::string
getText() const override;
/** Serialize the asset to a JSON value suitable for RPC responses.
*
* @return A `json::Value` representing the asset, formatted per asset type.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Append the binary encoding of this asset to `s`.
*
* XRP: 160-bit zero currency sentinel only.
* IOU: 160-bit currency + 160-bit issuer AccountID.
* MPT: 160-bit issuer AccountID + 160-bit `noAccount()` sentinel + 32-bit sequence.
*
* @param s The Serializer to append bytes to.
*/
void
add(Serializer& s) const override;
/** Return `true` if `t` is an STIssue holding an equivalent asset.
*
* @param t The STBase to compare; downcast to STIssue internally.
* @return `true` if `t` is an STIssue whose asset compares equal to this one.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` if this field holds the default asset (XRP).
*
* A field absent from a ledger object is implicitly XRP, so `xrpIssue()`
* is the canonical default. MPT issuances are never considered default.
*
* @return `true` iff the held asset is `xrpIssue()`.
*/
[[nodiscard]] bool
isDefault() const override;
/** Compare two STIssue objects for equality. Delegates to `Asset::operator==`. */
friend constexpr bool
operator==(STIssue const& lhs, STIssue const& rhs);
/** Three-way comparison of two STIssue objects. Delegates to `Asset::operator<=>`.
*
* @note Across asset types, `MPTIssue` sorts before `Issue` (variant index order).
*/
friend constexpr std::weak_ordering
operator<=>(STIssue const& lhs, STIssue const& rhs);
/** Compare an STIssue directly to a raw Asset for equality.
*
* Avoids the need to unwrap the STIssue when comparing against an `Asset`
* value elsewhere in the engine.
*/
friend constexpr bool
operator==(STIssue const& lhs, Asset const& rhs);
/** Three-way comparison of an STIssue against a raw Asset.
*
* @note Across asset types, `MPTIssue` sorts before `Issue` (variant index order).
*/
friend constexpr std::weak_ordering
operator<=>(STIssue const& lhs, Asset const& rhs);
@@ -88,6 +206,17 @@ STIssue::STIssue(SField const& name, A const& asset) : STBase{name}, asset_{asse
Throw<std::runtime_error>("Invalid asset: currency and account native mismatch");
}
/** Construct an STIssue by parsing a JSON asset representation.
*
* Delegates to `assetFromJson()` to resolve the JSON into the appropriate
* `Asset` variant (`xrpIssue()`, an IOU `Issue`, or an `MPTIssue`), then
* wraps it in an STIssue bound to `name`. This is the canonical entry point
* when deserializing an issue field from an API request.
*
* @param name The SField to attach to the resulting STIssue.
* @param v A JSON value encoding an asset (XRP object, IOU object, or MPT object).
* @return An STIssue bound to `name` holding the parsed asset.
*/
STIssue
issueFromJson(SField const& name, json::Value const& v);

View File

@@ -10,50 +10,176 @@ namespace test {
class Invariants_test;
} // namespace test
/** The C++ representation of a single object in the XRPL ledger state (universally aliased as `SLE`).
*
* Each ledger entry lives in a `SHAMap` keyed by a 256-bit `key_`. The
* `type_` member names what the object is (account root, offer, escrow,
* trust line, etc.) and determines which `SOTemplate` governs its field
* layout. Construction from a `Keylet` looks up the registered
* `LedgerFormats` schema and throws immediately if the type is unknown,
* ensuring that an SLE always has a valid, self-consistent type.
*
* Declared `final`: ledger-entry type diversity is handled entirely through
* the `LedgerFormats` registration system at runtime, not through C++
* subclass hierarchies.
*
* @see SLE (alias below), Keylet, LedgerFormats
*/
class STLedgerEntry final : public STObject, public CountedObject<STLedgerEntry>
{
uint256 key_;
LedgerEntryType type_;
public:
/** Shared-pointer to a mutable ledger entry. */
using pointer = std::shared_ptr<STLedgerEntry>;
/** Const reference to a shared-pointer to a mutable ledger entry. */
using ref = std::shared_ptr<STLedgerEntry> const&;
/** Shared-pointer to an immutable ledger entry. */
using const_pointer = std::shared_ptr<STLedgerEntry const>;
/** Const reference to a shared-pointer to an immutable ledger entry. */
using const_ref = std::shared_ptr<STLedgerEntry const> const&;
/** Create an empty object with the given key and type. */
/** Construct a new, empty ledger entry for the given keylet.
*
* Looks up the `LedgerFormats` schema for `k.type`, applies the
* `SOTemplate` (populating all declared fields at their default values),
* and writes `sfLedgerEntryType` so the wire encoding is self-describing.
*
* @param k Keylet carrying the SHAMap key and the desired entry type.
* @throws std::runtime_error if `k.type` is not registered in
* `LedgerFormats`.
*/
explicit STLedgerEntry(Keylet const& k);
/** Convenience constructor that delegates to the `Keylet` path.
*
* Equivalent to `STLedgerEntry(Keylet(type, key))`. Prefer the
* `Keylet` overload where one is already available.
*
* @param type The ledger entry type.
* @param key SHAMap key for this entry.
* @throws std::runtime_error if `type` is not registered in
* `LedgerFormats`.
*/
STLedgerEntry(LedgerEntryType type, uint256 const& key);
/** Deserialize a ledger entry from a byte stream.
*
* Reads all fields from `sit` into the underlying `STObject`, then
* resolves `sfLedgerEntryType` to set `type_` and enforces the matching
* `SOTemplate` via `setSLEType()`.
*
* @param sit Wire-format byte cursor; consumed in place.
* @param index SHAMap key that addresses this entry in the ledger state.
* @throws std::runtime_error if the deserialized type is unrecognized or
* the field set does not conform to the declared template.
*/
STLedgerEntry(SerialIter& sit, uint256 const& index);
/** Convenience rvalue overload that forwards to the lvalue `SerialIter` constructor.
*
* `SerialIter` is consumed by position rather than by move semantics, so
* this overload simply binds the rvalue to an lvalue reference and
* delegates.
*
* @param sit Wire-format byte cursor; consumed in place.
* @param index SHAMap key that addresses this entry in the ledger state.
* @throws std::runtime_error if the deserialized type is unrecognized or
* the field set does not conform to the declared template.
*/
STLedgerEntry(SerialIter&& sit, uint256 const& index);
/** Promote a pre-populated `STObject` to a typed ledger entry.
*
* Used when fields have already been parsed into a generic `STObject`
* and need to be re-interpreted with a concrete `LedgerEntryType`.
* Delegates to `setSLEType()` for type resolution and template
* conformance.
*
* @param object The source object, copied into this entry.
* @param index SHAMap key that addresses this entry in the ledger state.
* @throws std::runtime_error if `sfLedgerEntryType` is absent,
* unrecognized, or the field set does not conform to the declared
* template.
*/
STLedgerEntry(STObject const& object, uint256 const& index);
/** Return the serialized type identifier for ledger entries (`STI_LEDGERENTRY`). */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Return a verbose diagnostic string containing the type name, key, and all field values.
*
* Re-validates `type_` against `LedgerFormats` as a defensive invariant
* check before emitting output.
*
* @throws std::runtime_error if `type_` is no longer recognized
* (indicates in-memory corruption).
*/
[[nodiscard]] std::string
getFullText() const override;
/** Return a compact diagnostic string containing the hex key and field contents. */
[[nodiscard]] std::string
getText() const override;
/** Serialize this entry to JSON, augmenting the base `STObject` output.
*
* Injects `"index"` (the hex-encoded SHAMap key) because `key_` is not
* stored as a serialized field. For `ltMPTOKEN_ISSUANCE` objects, also
* injects `"mpt_issuance_id"` computed from `sfSequence` and `sfIssuer`
* — this derived identifier is not stored on-ledger and is recomputed
* on every read to keep consensus-critical storage non-redundant.
*
* @param options Controls JSON formatting (e.g., binary vs. human-readable).
* @return A `Json::Value` object with all fields plus the injected keys.
*/
[[nodiscard]] json::Value
getJson(JsonOptions options = JsonOptions::Values::None) const override;
/** Returns the 'key' (or 'index') of this item.
The key identifies this entry's position in
the SHAMap associative container.
*/
/** Return the 256-bit SHAMap key that locates this entry in the ledger state. */
[[nodiscard]] uint256 const&
key() const;
/** Return the `LedgerEntryType` that identifies what kind of object this is. */
[[nodiscard]] LedgerEntryType
getType() const;
// is this a ledger entry that can be threaded
/** Determine whether this entry participates in transaction threading.
*
* Threading links each ledger entry to the transaction that last modified
* it via `sfPreviousTxnID` / `sfPreviousTxnLgrSeq`. Five types
* (`ltDIR_NODE`, `ltAMENDMENTS`, `ltFEE_SETTINGS`, `ltNEGATIVE_UNL`,
* `ltAMM`) only gained `sfPreviousTxnID` support when the
* `fixPreviousTxnID` amendment activated; before that, this method
* returns `false` for those types even if the field technically exists in
* the template, preventing premature use of threading on objects that
* historically lacked it.
*
* @param rules The active amendment rules for the current ledger.
* @return `true` if this entry carries `sfPreviousTxnID` and threading
* is permitted under the current rules.
*/
[[nodiscard]] bool
isThreadedType(Rules const& rules) const;
/** Update the threading fields to record that `txID` last modified this entry.
*
* Reads the current `sfPreviousTxnID`; if it already equals `txID`, the
* transaction has already been applied to this entry — asserts that
* `sfPreviousTxnLgrSeq` also matches and returns `false` (idempotency
* guard against double-application). Otherwise writes `txID` and
* `ledgerSeq` into the object and captures the old values in the output
* parameters so callers can reconstruct the modification chain.
*
* @param txID Hash of the transaction that is modifying this entry.
* @param ledgerSeq Sequence number of the ledger containing `txID`.
* @param prevTxID [out] The previous value of `sfPreviousTxnID`.
* @param prevLedgerID [out] The previous value of `sfPreviousTxnLgrSeq`.
* @return `true` if the fields were updated; `false` if `txID` was
* already threaded and the entry is unchanged.
*/
bool
thread(
uint256 const& txID,
@@ -62,9 +188,17 @@ public:
std::uint32_t& prevLedgerID);
private:
/* Make STObject comply with the template for this SLE type
Can throw
*/
/** Resolve `type_` from the embedded `sfLedgerEntryType` field and enforce
* template conformance on an already-populated object.
*
* Post-hoc counterpart to the `set(SOTemplate)` call in the `Keylet`
* constructor: instead of initializing an empty object, it validates
* and conforms an already-populated one. Called after wire
* deserialization and after `STObject` promotion.
*
* @throws std::runtime_error if `sfLedgerEntryType` names an unrecognized
* type, or if `applyTemplate()` rejects the field set.
*/
void
setSLEType();
@@ -79,6 +213,7 @@ private:
friend class detail::STVar;
};
/** Canonical short alias for `STLedgerEntry`, used pervasively throughout the codebase. */
using SLE = STLedgerEntry;
inline STLedgerEntry::STLedgerEntry(LedgerEntryType type, uint256 const& key)
@@ -93,10 +228,6 @@ inline STLedgerEntry::STLedgerEntry(
{
}
/** Returns the 'key' (or 'index') of this item.
The key identifies this entry's position in
the SHAMap associative container.
*/
inline uint256 const&
STLedgerEntry::key() const
{

View File

@@ -9,27 +9,26 @@
namespace xrpl {
/**
* A serializable number.
/** Serializable asset-contextual numeric field for XRPL ledger objects.
*
* This type is-a `Number`, and can be used everywhere that is accepted.
* This type simply integrates `Number` with the serialization framework,
* letting it be used for fields in ledger entries and transactions.
* It is effectively an `STAmount` sans `Asset`:
* it can represent a value of any token type (XRP, IOU, or MPT)
* without paying the storage cost of duplicating asset information
* that may be deduced from the context.
* `STNumber` is effectively an `STAmount` without embedded `Asset` metadata.
* It stores only a `Number` (signed 64-bit mantissa + 32-bit exponent, 12
* bytes on the wire) and defers asset identity to runtime via the
* `STTakesAsset` mixin. This eliminates the per-field storage cost of
* duplicating asset information that is already present in the containing
* ledger entry (Vault, LoanBroker, Loan). All `NUMBER`-type SFields carry
* the `sMD_NeedsAsset` metadata flag; the free function
* `associateAsset(STLedgerEntry&, Asset const&)` walks a ledger entry and
* calls `associateAsset` on each such field near the end of `doApply()`.
*
* STNumber derives from STTakesAsset, so that it can be associated with the
* related Asset during transaction processing. Which asset is relevant depends
* on the object and transaction. As of this writing, only Vault, LoanBroker,
* and Loan objects use STNumber fields. All of those fields represent amounts
* of the Vault's Asset, so they should be associated with the Vault's Asset.
* Because `STNumber` provides `operator Number() const`, it can be passed
* directly wherever a `Number` is expected.
*
* e.g.
* associateAsset(*loanSle, asset);
* associateAsset(*brokerSle, asset);
* associateAsset(*vaultSle, asset);
* @note After `associateAsset()` is called, the stored value is rounded to
* the asset's canonical precision. Calling `setValue()` afterward
* without re-associating violates the two-phase rounding contract and
* will trigger an assertion in `add()`.
* @see STTakesAsset, STAmount, associateAsset(STLedgerEntry&, Asset const&)
*/
class STNumber : public STTakesAsset, public CountedObject<STNumber>
{
@@ -40,21 +39,72 @@ public:
using value_type = Number;
STNumber() = default;
/** Construct an STNumber bound to the given SField with an initial value.
*
* @param field The SField that identifies this value in its containing
* object. Must have `fieldType == STI_NUMBER`.
* @param value Initial numeric value; defaults to `Number()` (zero with
* sentinel exponent `std::numeric_limits<int>::lowest()`).
*/
explicit STNumber(SField const& field, Number const& value = Number());
/** Deserialize an STNumber from a byte stream.
*
* Reads a 64-bit signed mantissa and a 32-bit signed exponent (12 bytes
* total) from @p sit. The two reads are issued as separate statements to
* guarantee evaluation order — merging them into a single call expression
* would produce undefined behavior because C++ does not sequence function
* arguments.
*
* @param sit Forward cursor positioned at the first byte of the payload.
* @param field The SField that identifies this value in its containing
* object.
*/
STNumber(SerialIter& sit, SField const& field);
/** @return `STI_NUMBER`. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** @return Decimal string representation of the stored `Number`. */
[[nodiscard]] std::string
getText() const override;
/** Serialize the stored value as 12 bytes (int64 mantissa, int32 exponent).
*
* For `sMD_NeedsAsset` fields this is Phase 2 of the two-phase rounding
* contract. When an asset has been associated, the value is re-rounded and
* asserted equal to the stored value, confirming that `associateAsset()`
* was called after the last `setValue()`. When no asset is present, a
* debug-only assertion verifies that `MantissaRange::Large` is active,
* because serializing under the small mantissa scale would silently
* truncate XRP/MPT integer values larger than 15 digits.
*
* @param s Serializer accumulator to append to.
*/
void
add(Serializer& s) const override;
/** @return Read-only reference to the stored `Number`. */
[[nodiscard]] Number const&
value() const;
/** Replace the stored value without re-associating an asset.
*
* @param v New value.
* @note If `associateAsset()` has already been called on this field,
* calling `setValue()` afterward without re-associating violates the
* two-phase rounding contract and will trigger an assertion in `add()`.
*/
void
setValue(Number const& v);
/** Assign a new value; delegates to `setValue()`.
*
* @param rhs New value.
* @return `*this`.
*/
STNumber&
operator=(Number const& rhs)
{
@@ -62,14 +112,38 @@ public:
return *this;
}
/** @return `true` if the other `STBase` is an `STNumber` holding the same
* `Number` value.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** @return `true` if the stored value equals the default-constructed
* `Number()` (zero with its sentinel exponent), ensuring zero-valued
* fields round-trip correctly without false positives.
*/
[[nodiscard]] bool
isDefault() const override;
/** Bind an asset and immediately round the stored value to its precision.
*
* Phase 1 of the two-phase rounding contract. Stores @p a via
* `STTakesAsset::associateAsset` then calls `roundToAsset(a, value_)`.
* For XRP and MPT this truncates fractional drops; for IOU this normalises
* to 15 significant decimal digits. After this call, `add()` will assert
* idempotency on the rounded value.
*
* @param a Asset whose precision governs rounding.
* @note The field must carry the `sMD_NeedsAsset` metadata flag; a debug
* assertion fires if it does not.
*/
void
associateAsset(Asset const& a) override;
/** Implicit conversion to `Number`, enabling use in numeric expressions.
*
* @return A copy of the stored `Number`.
*/
operator Number() const
{
return value_;
@@ -82,19 +156,68 @@ private:
move(std::size_t n, void* buf) override;
};
/** Write the decimal string representation of @p rhs to @p out.
*
* @param out Output stream to write to.
* @param rhs The value to render.
* @return @p out.
*/
std::ostream&
operator<<(std::ostream& out, STNumber const& rhs);
/** Raw parsed components of a decimal number string.
*
* Produced by `partsFromString()` before normalization. The mantissa is
* always unsigned; sign is carried separately in `negative`.
*/
struct NumberParts
{
/** Unsigned integer formed by concatenating integer and fractional digits. */
std::uint64_t mantissa = 0;
/** Exponent adjusted for fractional digit count and any explicit `e` suffix. */
int exponent = 0;
/** `true` if the original string had a leading `'-'`. */
bool negative = false;
};
/** Parse a decimal string into its raw mantissa/exponent/sign components.
*
* Accepts an optional leading sign, a non-empty integer part (no leading
* zeroes unless the value is exactly `"0"`), an optional fractional part,
* and an optional `e`/`E` exponent suffix. No normalization is applied —
* the caller receives the raw parsed representation.
*
* @param number Decimal string to parse (e.g., `"3.14e2"`, `"-42"`, `"0"`).
* @return `NumberParts` with unsigned mantissa, adjusted exponent, and sign.
* @throws std::runtime_error if @p number does not match the expected format
* (e.g., empty string, leading zeroes, bare `"e"`, trailing decimal point).
* @throws std::bad_cast (via `boost::lexical_cast`) if the digit string
* overflows `uint64_t`.
* @note The backing regex is compiled once as a `static` local with the
* `optimize` flag to amortize construction cost across calls.
*/
NumberParts
partsFromString(std::string const& number);
/** Construct an STNumber from a JSON integer or decimal string.
*
* Dispatches on the JSON value type:
* - **Integer** (`isInt`/`isUInt`): reads the native integer value directly.
* - **String**: delegates to `partsFromString()`; this path asserts that no
* active transaction rules are present, restricting use to pre-transactor
* JSON deserialization (e.g., `STParsedJSON`).
* - Anything else throws.
*
* @param field The SField that identifies the resulting STNumber.
* @param value JSON node containing the numeric value.
* @return A new STNumber holding the parsed value, not yet asset-rounded.
* @throws std::runtime_error if @p value is not an integer or string, or if
* a string fails to parse as a valid decimal.
* @throws std::bad_cast (via `boost::lexical_cast`) if a string mantissa
* overflows `uint64_t`.
* @note String-format numbers are forbidden during active transaction
* processing; only numeric JSON types are accepted in that context.
*/
STNumber
numberFromJson(SField const& field, json::Value const& value);

View File

@@ -1,3 +1,15 @@
/** @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>
@@ -27,15 +39,39 @@ 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>
{
// Proxy value for a STBase derived class
template <class T>
class Proxy;
template <class T>
@@ -43,6 +79,7 @@ class STObject : public STBase, public CountedObject<STObject>
template <class T>
class OptionalProxy;
/** Functor used by `boost::transform_iterator` to project `STVar→STBase`. */
struct Transform
{
explicit Transform() = default;
@@ -60,11 +97,22 @@ class STObject : public STBase, public CountedObject<STObject>
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)
{
@@ -73,128 +121,328 @@ public:
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);
// these throw if the field type doesn't match, or return default values
// if the field is optional but not present
/** @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
@@ -205,7 +453,6 @@ public:
getFieldU64(SField const& field) const;
[[nodiscard]] uint128
getFieldH128(SField const& field) const;
[[nodiscard]] uint160
getFieldH160(SField const& field) const;
[[nodiscard]] uint192
@@ -216,7 +463,6 @@ public:
getFieldI32(SField const& field) const;
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&
@@ -225,7 +471,12 @@ public:
getFieldPathSet(SField const& field) const;
[[nodiscard]] STVector256 const&
getFieldV256(SField const& field) const;
// If not found, returns an object constructed with the given field
/** 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&
@@ -234,6 +485,7 @@ public:
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
@@ -329,15 +581,31 @@ public:
OptionalProxy<T>
at(OptionaledField<T> const& of);
/** Set a field.
if the field already exists, it is replaced.
*/
/** 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
@@ -358,10 +626,8 @@ public:
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
@@ -379,40 +645,122 @@ public:
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)
@@ -474,15 +822,34 @@ private:
//------------------------------------------------------------------------------
/** 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;
@@ -500,36 +867,65 @@ protected:
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);
};
// Constraint += and -= ValueProxy operators
// to value types that support arithmetic operations
/** 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>
{
@@ -541,22 +937,24 @@ public:
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);
// Convenience operators for value types supporting
// arithmetic operations
/** 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>
@@ -572,6 +970,22 @@ private:
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>
{
@@ -593,6 +1007,7 @@ public:
explicit
operator bool() const noexcept;
/** Implicit conversion to `std::optional<value_type>`. */
operator optional_type() const;
/** Explicit conversion to std::optional */
@@ -665,17 +1080,26 @@ public:
return !(lhs == rhs);
}
// Emulate std::optional::value_or
/** 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);
@@ -685,12 +1109,15 @@ private:
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;
};

View File

@@ -6,19 +6,50 @@
namespace xrpl {
/** Holds the serialized result of parsing an input JSON object.
This does validation and checking on the provided JSON.
*/
/** Single-use converter from a JSON object to an @ref STObject.
*
* Sits at the boundary between the JSON/RPC layer and the binary-canonical
* Serialized Type (ST) system. In a single constructor call it validates
* every field name against the XRPL protocol schema, coerces each value to
* its wire type, recurses into nested objects and arrays up to 64 levels
* deep, and applies the field template for the transaction or ledger-entry
* type discovered during parsing.
*
* Outcomes are communicated through two public members rather than via
* exceptions or a return value, which lets RPC handlers forward `error`
* directly to the client without additional formatting work:
* - On success: `object` holds the populated `STObject`; `error` is empty.
* - On failure: `object` is `std::nullopt`; `error` is an
* `rpcINVALID_PARAMS` JSON value with a dot-separated field path
* (e.g. `"tx_json.Signers[0].Signer.Account"`) pinpointing the offending
* field.
*
* The class is non-copyable and not default-constructible; every instance
* represents exactly one completed parse attempt.
*
* @see TransactionSign.cpp for the primary production call-site.
*/
class STParsedJSONObject
{
public:
/** Parses and creates an STParsedJSON object.
The result of the parsing is stored in object and error.
Exceptions:
Does not throw.
@param name The name of the JSON field, used in diagnostics.
@param json The JSON-RPC to parse.
*/
/** Parse @p json into a strongly-typed @ref STObject.
*
* Iterates every member of the JSON object, resolves each field name
* via `SField::getField()`, recurses into nested objects and arrays,
* and finally calls `applyTemplateFromSField()` to enforce the field
* template for the detected transaction or ledger-entry type.
*
* All internal exceptions are caught and translated into a structured
* `rpcINVALID_PARAMS` error stored in `error`; nothing propagates to
* the caller.
*
* @param name The logical name of the top-level field being parsed
* (e.g. `"tx_json"`); used as the root of dot-separated field-path
* strings in `error` messages.
* @param json The JSON object to parse. Must be a JSON object value;
* non-object input produces an `rpcINVALID_PARAMS` error.
* @note Does not throw.
*/
STParsedJSONObject(std::string const& name, json::Value const& json);
STParsedJSONObject() = delete;
@@ -27,10 +58,15 @@ public:
operator=(STParsedJSONObject const&) = delete;
~STParsedJSONObject() = default;
/** The STObject if the parse was successful. */
/** The parsed object on success, or `std::nullopt` on any parse error. */
std::optional<STObject> object;
/** On failure, an appropriate set of error values. */
/** Structured `rpcINVALID_PARAMS` error on failure; empty on success.
*
* The JSON value is suitable for forwarding directly to an RPC client.
* The `"error_message"` field contains a dot-separated field path
* identifying the first field that failed to parse.
*/
json::Value error;
};

View File

@@ -1,3 +1,14 @@
/** @file
* Defines the three-class hierarchy for payment path representation in XRPL
* transactions.
*
* `STPathElement` is a single hop; `STPath` is an ordered sequence of hops;
* `STPathSet` is the collection of alternate candidate paths carried in the
* `Paths` field of a `Payment` transaction on the wire. Together they encode
* how a cross-currency payment routes through the order book and the trust-line
* graph from source to destination.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -14,6 +25,24 @@
namespace xrpl {
/** A single hop in a payment path.
*
* A node is either an *account node* (rippling through trust lines) or an
* *offer node* (matching against a DEX order book). `isOffer()` returns `true`
* when `mAccountID` is the XRP "no account" sentinel; otherwise the element
* represents an account. The `Type` bitmask drives both on-wire encoding and
* runtime dispatch.
*
* The asset field holds a `PathAsset` variant (`Currency` for legacy IOU hops,
* `MPTID` for MPT hops). `TypeCurrency` and `TypeMpt` are mutually exclusive.
*
* A non-cryptographic hash of the account, asset, and issuer fields is
* pre-computed at construction (`hash_value_`). `operator==` short-circuits on
* this hash before performing field-by-field comparison, making duplicate
* detection in the pathfinder fast over the small vectors used in practice.
*
* @see STPath, STPathSet
*/
class STPathElement final : public CountedObject<STPathElement>
{
unsigned int type_;
@@ -25,97 +54,217 @@ class STPathElement final : public CountedObject<STPathElement>
std::size_t hash_value_;
public:
// Bitwise values (typeCurrency | typeMPT)
/** Bitmask constants that govern on-wire encoding and runtime dispatch.
*
* Each hop's type byte is the OR of the applicable constants. The
* deserializer rejects any byte with bits outside `TypeAll` as malformed.
* `TypeCurrency` and `TypeMpt` are mutually exclusive; `TypeAsset` is a
* convenience mask to test either without caring which.
*/
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Type {
TypeNone = 0x00,
TypeAccount = 0x01, // Rippling through an account (vs taking an offer).
TypeCurrency = 0x10, // Currency follows.
TypeIssuer = 0x20, // Issuer follows.
TypeMpt = 0x40, // MPT follows.
TypeBoundary = 0xFF, // Boundary between alternate paths.
TypeAsset = TypeCurrency | TypeMpt,
TypeAll = TypeAccount | TypeCurrency | TypeIssuer | TypeMpt,
// Combination of all types.
TypeNone = 0x00, /**< Path terminator (0x00 byte ends the PathSet). */
TypeAccount = 0x01, /**< Account field is present; node ripples through trust lines. */
TypeCurrency = 0x10, /**< Legacy IOU Currency (160-bit) follows. Mutually exclusive with TypeMpt. */
TypeIssuer = 0x20, /**< Issuer AccountID (160-bit) follows. */
TypeMpt = 0x40, /**< MPT issuance ID (192-bit MPTID) follows. Mutually exclusive with TypeCurrency. */
TypeBoundary = 0xFF, /**< Separator between consecutive paths within the PathSet. */
TypeAsset = TypeCurrency | TypeMpt, /**< Either asset kind; tests presence without distinguishing IOU vs MPT. */
TypeAll = TypeAccount | TypeCurrency | TypeIssuer | TypeMpt, /**< Union of all valid type bits; used to validate incoming bytes. */
};
/** Construct a `TypeNone` (path-terminator / empty) element.
*
* The resulting element has `is_offer_ = true` and all fields zero.
* Used as a sentinel and by default-constructed STPath entries.
*/
STPathElement();
STPathElement(STPathElement const&) = default;
STPathElement&
operator=(STPathElement const&) = default;
/** Construct an element from optional fields, setting type bits automatically.
*
* The type bitmask is derived from which optionals are non-null:
* `TypeAccount` if `account` is set, `TypeCurrency`/`TypeMpt` from the
* `PathAsset` variant if `asset` is set, and `TypeIssuer` if `issuer` is
* set. Asserts (debug builds) that account and issuer are not `noAccount()`
* when provided.
*
* @param account AccountID of the hop; absent means this is an offer node.
* @param asset PathAsset (Currency or MPTID) for the hop; absent means
* no asset constraint.
* @param issuer Issuer AccountID; absent means no issuer constraint.
*/
STPathElement(
std::optional<AccountID> const& account,
std::optional<PathAsset> const& asset,
std::optional<AccountID> const& issuer);
/** Construct an element from explicit non-optional fields.
*
* `TypeAccount` is set when `account` is not the XRP sentinel; the asset
* type bit (`TypeCurrency` or `TypeMpt`) is set when the asset is not XRP
* (or unconditionally when `forceAsset` is `true`); `TypeIssuer` is set
* when `issuer` is not the XRP sentinel.
*
* @param account AccountID of the hop; XRP sentinel (`xrpAccount()`)
* means offer node.
* @param asset PathAsset describing the hop's currency or MPT.
* @param issuer Issuer AccountID.
* @param forceAsset When `true`, always set the asset type bit even if the
* asset is XRP. Used to preserve currency information in offer nodes
* whose asset happens to be XRP.
*/
STPathElement(
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer,
bool forceAsset = false);
/** Construct an element from an explicit wire-format type byte and fields.
*
* Used by the deserializer. The type byte is accepted verbatim and then
* sanitised: the actual `PathAsset` variant is inspected to clear the
* contradictory bit (`TypeMpt` when holding a `Currency`, or
* `TypeCurrency` when holding an `MPTID`), so a caller cannot pass a
* self-contradictory bitmask.
*
* @param uType Wire-format type bitmask from the stream.
* @param account AccountID of the hop.
* @param asset PathAsset (Currency or MPTID) for the hop.
* @param issuer Issuer AccountID.
*/
STPathElement(
unsigned int uType,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer);
/** Return the raw type bitmask for this element.
*
* The result is the OR of the applicable `Type` constants and can be
* inspected with `isType()` or tested directly against the `Type` enum.
*/
[[nodiscard]] auto
getNodeType() const;
/** Return `true` if this element represents a DEX offer node.
*
* An element is an offer node when its account field is the XRP "no
* account" sentinel, meaning the hop matches against the order book
* rather than rippling through a trust line.
*/
[[nodiscard]] bool
isOffer() const;
/** Return `true` if this element represents a trust-line account node.
*
* Equivalent to `!isOffer()`.
*/
[[nodiscard]] bool
isAccount() const;
/** Return `true` if the `TypeIssuer` bit is set. */
[[nodiscard]] bool
hasIssuer() const;
/** Return `true` if the `TypeCurrency` bit is set (legacy IOU hop). */
[[nodiscard]] bool
hasCurrency() const;
/** Return `true` if the `TypeMpt` bit is set (MPT hop). */
[[nodiscard]] bool
hasMPT() const;
/** Return `true` if any asset type bit (`TypeCurrency` or `TypeMpt`) is set. */
[[nodiscard]] bool
hasAsset() const;
/** Return `true` if this element is a path terminator (`TypeNone`). */
[[nodiscard]] bool
isNone() const;
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
/** Return the account for this hop.
*
* For account nodes this is the AccountID through which the payment
* ripples. For offer nodes the field holds the XRP "no account" sentinel
* and callers should use `isOffer()` to distinguish the two cases before
* interpreting this value.
*/
[[nodiscard]] AccountID const&
getAccountID() const;
/** Return the `PathAsset` (Currency or MPTID) for this hop. */
[[nodiscard]] PathAsset const&
getPathAsset() const;
/** Return the `Currency` for this hop.
*
* @note Only valid when `hasCurrency()` is `true`; the underlying
* `PathAsset::get<Currency>()` throws on type mismatch in debug builds.
*/
[[nodiscard]] Currency const&
getCurrency() const;
/** Return the `MPTID` for this hop.
*
* @note Only valid when `hasMPT()` is `true`; the underlying
* `PathAsset::get<MPTID>()` throws on type mismatch in debug builds.
*/
[[nodiscard]] MPTID const&
getMPTID() const;
/** Return the issuer AccountID for this hop. */
[[nodiscard]] AccountID const&
getIssuerID() const;
/** Return `true` if any bit of `pe` is set in the element's type bitmask.
*
* @param pe Type mask to test; typically a single `Type` constant or a
* bitwise OR of several.
*/
[[nodiscard]] bool
isType(Type const& pe) const;
/** Return `true` if the two elements have identical account, asset, and issuer fields.
*
* Short-circuits first on the `TypeAccount` bit and the pre-computed hash
* before performing full field comparison, making deduplication fast over
* the small path vectors used in practice.
*/
bool
operator==(STPathElement const& t) const;
/** Return `true` if the two elements differ in any field. */
bool
operator!=(STPathElement const& t) const;
private:
/** Compute the non-cryptographic hash stored in `hash_value_`.
*
* Uses FNV-style multiply-XOR with distinct primes (257, 509, 911) for
* the account, asset, and issuer fields respectively, then XORs the three
* sub-hashes together. Reads the actual `PathAsset` variant via `visit()`
* rather than the type bitmask, because the bitmask may be partially set
* during pathfinder construction. Speed dominates; cryptographic strength
* is not required.
*
* @param element The element to hash.
* @return Non-cryptographic hash combining account, asset, and issuer.
*/
static std::size_t
getHash(STPathElement const& element);
};
/** An ordered sequence of `STPathElement` hops describing one candidate payment path.
*
* Wraps a `std::vector<STPathElement>` with a standard container interface.
* The XRPL protocol caps path length, so the underlying vector is short in
* practice (typically 26 elements); linear scans are therefore acceptable.
*
* @see STPathElement, STPathSet
*/
class STPath final : public CountedObject<STPath>
{
std::vector<STPathElement> path_;
@@ -123,54 +272,111 @@ class STPath final : public CountedObject<STPath>
public:
STPath() = default;
/** Construct a path from an existing vector of elements.
*
* @param p Elements to populate the path; moved into internal storage.
*/
STPath(std::vector<STPathElement> p);
/** Return the number of hops in this path. */
[[nodiscard]] std::vector<STPathElement>::size_type
size() const;
/** Return `true` if the path contains no hops. */
[[nodiscard]] bool
empty() const;
/** Append a copy of `e` to the end of this path. */
void
pushBack(STPathElement const& e);
/** Emplace a new element at the end of this path.
*
* @tparam Args Argument types forwarded to `STPathElement`'s constructor.
* @param args Arguments forwarded to the new element's constructor.
*/
template <typename... Args>
void
emplaceBack(Args&&... args);
/** Return `true` if any hop in this path matches the given (account, asset, issuer) triple.
*
* Used by the pathfinder for cycle detection: before extending a path,
* `Pathfinder::addLink()` calls this to ensure the candidate hop has not
* already appeared earlier in the path. A linear scan is acceptable
* because XRPL path lengths are protocol-bounded.
*
* @param account AccountID of the hop to search for.
* @param asset PathAsset (Currency or MPTID) to match.
* @param issuer Issuer AccountID to match.
* @return `true` if any existing element equals all three arguments.
*/
[[nodiscard]] bool
hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const;
/** Serialize the path to a JSON array of hop objects.
*
* Each hop object always includes a `type` field. Optional `account`,
* `currency`, `mpt_issuance_id`, and `issuer` keys are present only when
* the corresponding type bit is set.
*
* @return JSON array where each element describes one hop.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const;
/** Return an iterator to the first element. */
[[nodiscard]] std::vector<STPathElement>::const_iterator
begin() const;
/** Return a past-the-end iterator. */
[[nodiscard]] std::vector<STPathElement>::const_iterator
end() const;
/** Return `true` if both paths contain identical elements in identical order. */
bool
operator==(STPath const& t) const;
/** Return a reference to the last element. */
[[nodiscard]] std::vector<STPathElement>::const_reference
back() const;
/** Return a reference to the first element. */
[[nodiscard]] std::vector<STPathElement>::const_reference
front() const;
/** Return a mutable reference to the element at index `i`. */
STPathElement&
operator[](int i);
/** Return a const reference to the element at index `i`. */
STPathElement const&
operator[](int i) const;
/** Reserve capacity for `s` elements, avoiding reallocations during path construction.
*
* @param s Minimum capacity to reserve.
*/
void
reserve(size_t s);
};
//------------------------------------------------------------------------------
// A set of zero or more payment paths
/** The serialized `Paths` field of a Payment transaction — a collection of alternate payment paths.
*
* Inherits from `STBase` and participates in the ledger type system via
* `STI_PATHSET`. The binary wire format encodes each `STPath` as a sequence
* of type-tagged hop records; consecutive paths are delimited by
* `TypeBoundary` (0xFF) and the entire field is terminated by `TypeNone`
* (0x00). Deserialization throws `std::runtime_error` on malformed input
* (empty paths or unknown type bits).
*
* The `isDefault()` override returns `true` when the set is empty, allowing
* the serialization layer to elide the field from transactions that have no
* explicit paths.
*
* @see STPath, STPathElement
*/
class STPathSet final : public STBase, public CountedObject<STPathSet>
{
std::vector<STPath> value_;
@@ -178,55 +384,140 @@ class STPathSet final : public STBase, public CountedObject<STPathSet>
public:
STPathSet() = default;
/** Construct an empty STPathSet named by `n`. */
STPathSet(SField const& n);
/** Deserialize an STPathSet from a binary stream.
*
* Reads the wire format produced by `add()`: hop records delimited by
* `TypeBoundary` (0xFF) and terminated by `TypeNone` (0x00).
*
* @param sit Binary cursor positioned at the first type byte; advanced
* past the terminating `TypeNone` on return.
* @param name SField that names this field in the enclosing object.
* @throws std::runtime_error "empty path" if a boundary or terminator is
* encountered before any hop is accumulated.
* @throws std::runtime_error "bad path element" if a type byte contains
* bits outside `TypeAll`.
*/
STPathSet(SerialIter& sit, SField const& name);
/** Serialize the path set to its canonical binary wire format.
*
* Emits each hop as a type byte followed by its optional account (20B),
* MPTID (24B), currency (20B), and/or issuer (20B) payloads. Consecutive
* paths are separated by `TypeBoundary` (0xFF); the set ends with
* `TypeNone` (0x00).
*
* @param s Serializer accumulator to which bytes are appended.
*/
void
add(Serializer& s) const override;
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Serialize the path set to a JSON array of path arrays.
*
* @param options JSON rendering options forwarded to each path.
* @return Nested JSON array: `[[hop, ...], [hop, ...], ...]`.
*/
[[nodiscard]] json::Value getJson(JsonOptions options) const override;
/** Return `STI_PATHSET`, identifying this field to the serialization framework. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Append `base` extended by `tail` to the set, unless an identical path already exists.
*
* Used by the pathfinder to build candidate paths incrementally. The
* candidate is pushed onto `value_` and then scanned against existing
* paths in reverse order (newest-first) to detect duplicates; if found,
* the candidate is popped and `false` is returned. Reverse iteration is a
* micro-optimisation because duplicates are most likely among recently
* added paths.
*
* @param base Prefix path to extend.
* @param tail Single hop appended to `base` before comparison.
* @return `true` if the path was added; `false` if it was a duplicate.
*/
bool
assembleAdd(STPath const& base, STPathElement const& tail);
/** Return `true` if `t` is an STPathSet with identical path contents.
*
* @param t Object to compare; returns `false` immediately if not an STPathSet.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return `true` when the set contains no paths.
*
* The serialization layer uses this to elide the `Paths` field from
* transactions that require no explicit pathfinding routes.
*/
[[nodiscard]] bool
isDefault() const override;
// std::vector like interface:
// std::vector-like interface for iterating and indexing paths.
/** Return a const reference to the path at index `n`. */
std::vector<STPath>::const_reference
operator[](std::vector<STPath>::size_type n) const;
/** Return a mutable reference to the path at index `n`. */
std::vector<STPath>::reference
operator[](std::vector<STPath>::size_type n);
/** Return an iterator to the first path. */
[[nodiscard]] std::vector<STPath>::const_iterator
begin() const;
/** Return a past-the-end iterator. */
[[nodiscard]] std::vector<STPath>::const_iterator
end() const;
/** Return the number of paths in the set. */
[[nodiscard]] std::vector<STPath>::size_type
size() const;
/** Return `true` if the set contains no paths. */
[[nodiscard]] bool
empty() const;
/** Append a copy of `e` to the set.
*
* @note Does not deduplicate; use `assembleAdd()` when deduplication is needed.
*/
void
pushBack(STPath const& e);
/** Emplace a new path at the end of the set.
*
* @tparam Args Argument types forwarded to `STPath`'s constructor.
* @param args Arguments forwarded to the new path's constructor.
*/
template <typename... Args>
void
emplaceBack(Args&&... args);
private:
/** Copy-construct this STPathSet into `buf` via placement-new.
*
* Plugs STPathSet into the `detail::STVar` small-object storage scheme.
*
* @param n Byte size of `buf`; must be at least `sizeof(STPathSet)`.
* @param buf Aligned destination buffer.
* @return Pointer to the newly constructed object.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Move-construct this STPathSet into `buf` via placement-new.
*
* Plugs STPathSet into the `detail::STVar` small-object storage scheme.
*
* @param n Byte size of `buf`; must be at least `sizeof(STPathSet)`.
* @param buf Aligned destination buffer.
* @return Pointer to the newly constructed object.
*/
STBase*
move(std::size_t n, void* buf) override;
@@ -369,8 +660,6 @@ STPathElement::isNone() const
return getNodeType() == STPathElement::TypeNone;
}
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
inline AccountID const&
STPathElement::getAccountID() const
{
@@ -499,7 +788,6 @@ inline STPathSet::STPathSet(SField const& n) : STBase(n)
{
}
// std::vector like interface:
inline std::vector<STPath>::const_reference
STPathSet::operator[](std::vector<STPath>::size_type n) const
{

View File

@@ -5,28 +5,48 @@
namespace xrpl {
/** Intermediate class for any STBase-derived class to store an Asset.
/** Mixin base that lets a serializable field receive an `Asset` at runtime.
*
* In the class definition, this class should be specified as a base class
* _instead_ of STBase.
* Derived classes inherit from `STTakesAsset` _instead of_ `STBase` when
* they store a numeric quantity whose precision depends on the enclosing
* ledger entry's asset type (XRP, IOU, or MPT). The asset identity is
* already present in the containing ledger object and must not be duplicated
* in each field; `STTakesAsset` carries it at runtime without serializing it.
*
* Specifically, the Asset is only stored and used at runtime. It should not be
* serialized to the ledger.
* The only current concrete user is `STNumber`, which overrides
* `associateAsset()` to round its stored `Number` to the asset's canonical
* precision immediately upon association and again during serialization.
*
* The derived class decides what to do with the Asset, and when. It will not
* necessarily be set at any given time. As of this writing, only STNumber uses
* it to round the stored Number to the Asset's precision both when associated,
* and when serializing the Number.
* @note `asset_` is intentionally `std::optional`: during deserialization from
* disk no transactor context is available, so no asset can be supplied.
* The value still round-trips correctly because it was already rounded when
* originally written.
* @see STNumber, associateAsset(STLedgerEntry&, Asset const&)
*/
class STTakesAsset : public STBase
{
protected:
/** Runtime asset identity used for precision rounding.
*
* Absent (`std::nullopt`) on the deserialization path; set by a call to
* `associateAsset()` inside `doApply()` before the SLE is serialized.
*/
std::optional<Asset> asset_;
public:
using STBase::STBase;
using STBase::operator=;
/** Record @p a as the asset governing this field's precision.
*
* The base implementation stores @p a in `asset_` via `emplace` and
* returns. Derived classes override this method to act on the asset
* immediately — for example, `STNumber` also rounds its stored value to
* the asset's canonical precision.
*
* @param a The asset to associate. Must be the same asset used by all
* other `sMD_NeedsAsset`-flagged fields in the enclosing SLE.
*/
virtual void
associateAsset(Asset const& a);
};
@@ -39,20 +59,26 @@ STTakesAsset::associateAsset(Asset const& a)
class STLedgerEntry;
/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
/** Associate an asset with every `sMD_NeedsAsset`-flagged field in @p sle.
*
* This function iterates over all fields in the given ledger entry. For each
* field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls
* `associateAsset` on that field with the given Asset. Such field must be
* derived from STTakesAsset - if it is not, the conversion will throw.
* Iterates over all fields in @p sle by offset (the only path that yields
* mutable `STBase&` references). For each field that is present and carries
* `SField::kSMD_NEEDS_ASSET`, calls `associateAsset(asset)` on it, triggering
* derived-class rounding logic (e.g., `STNumber` rounds to the asset's
* canonical precision). After rounding, any `soeDEFAULT`-style field whose
* value has become the default (e.g., rounded down to zero) is removed from
* the SLE via `makeFieldAbsent` so that zero defaults are not persisted in the
* ledger.
*
* Typically, associateAsset should be called near the end of doApply() of any
* Transactor classes on the SLEs of any new or modified ledger entries
* containing STNumber fields, after doing all of the modifications t the SLEs.
*
* @param sle The ledger entry whose fields will be updated.
* @param asset The Asset to associate with the relevant fields.
* Call this near the end of `doApply()` in any transactor that creates or
* modifies an SLE containing `STNumber` fields, after all other mutations to
* the SLE are complete. Rounding before computations finish may distort
* intermediate values.
*
* @param sle The ledger entry whose `sMD_NeedsAsset` fields will be updated.
* @param asset The asset that governs precision for all such fields in @p sle.
* @throws std::bad_cast if any field carrying `kSMD_NEEDS_ASSET` is not
* derived from `STTakesAsset` — this indicates a field schema error.
*/
void
associateAsset(STLedgerEntry& sle, Asset const& asset);

View File

@@ -1,3 +1,8 @@
/** @file
* Declares `STTx`, the canonical in-memory representation of an XRP Ledger
* transaction, together with the free functions that operate on it
* (`passesLocalChecks`, `sterilize`, `isPseudoTx`).
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -15,67 +20,171 @@
namespace xrpl {
/** Status codes used to tag a transaction row in the local SQLite
* `Transactions` table.
*
* Each enumerator maps to the single-character `Status` column value
* stored by `getMetaSQL`.
*/
enum class TxnSql : char {
New = 'N',
Conflict = 'C',
Held = 'H',
Validated = 'V',
Included = 'I',
Unknown = 'U'
New = 'N', /**< Transaction has just been received and not yet processed. */
Conflict = 'C', /**< Transaction conflicts with a previously applied transaction. */
Held = 'H', /**< Transaction is queued but not yet eligible for inclusion. */
Validated = 'V', /**< Transaction is in a validated ledger. */
Included = 'I', /**< Transaction is included in a pending ledger. */
Unknown = 'U' /**< Transaction status cannot be determined. */
};
/** The canonical in-memory representation of an XRP Ledger transaction.
*
* `STTx` extends `STObject` with transaction-specific identity, typing,
* signing, and persistence semantics. It caches the transaction ID (`tid_`,
* a SHA-512 half-hash prefixed with `HashPrefix::transactionID`) and the
* decoded transaction type (`tx_type_`) so that hot paths avoid repeated
* field lookups and hash recomputation.
*
* Three construction paths exist: wire deserialization (`SerialIter&`),
* object promotion (`STObject&&`), and programmatic assembly
* (`TxType, assembler`). Copy construction is allowed; copy assignment is
* deleted to prevent invariant violations on re-assignment.
*
* The class is `final`: transaction-type-specific behavior lives in the
* transactor subsystem, not in subclasses of `STTx`.
*
* @note `CountedObject<STTx>` tracks live instance counts for diagnostics.
*/
class STTx final : public STObject, public CountedObject<STTx>
{
uint256 tid_;
TxType tx_type_;
public:
/** Minimum number of signers allowed in a multi-sign signer list. */
static constexpr std::size_t kMIN_MULTI_SIGNERS = 1;
/** Maximum number of signers allowed in a multi-sign signer list. */
static constexpr std::size_t kMAX_MULTI_SIGNERS = 32;
STTx() = delete;
STTx(STTx const& other) = default;
/** Deleted to prevent re-assignment from invalidating the cached ID and type. */
STTx&
operator=(STTx const& other) = delete;
/** Deserialize a transaction from a wire-format byte stream.
*
* Validates the remaining byte count against the
* `kTX_MIN_SIZE_BYTES`/`kTX_MAX_SIZE_BYTES` protocol bounds, parses
* the field stream, applies the `SOTemplate` for the decoded
* `TxType`, and caches the transaction ID. This is the hottest
* construction path: every inbound peer transaction and every
* transaction loaded from the node store passes through here.
*
* @param sit A `SerialIter` positioned at the first byte of the
* serialized transaction. The iterator is advanced in place.
* @throws std::runtime_error if the byte count is outside protocol
* bounds, if an object-terminator byte is encountered at the top
* level, if the transaction type is unregistered, or if
* `applyTemplate` rejects the field layout.
*/
explicit STTx(SerialIter& sit);
/** Rvalue-reference overload that delegates to the lvalue constructor.
*
* `SerialIter` is consumed by value semantics internally, so the
* rvalue is not actually moved; the `// NOLINT` in the inline
* definition acknowledges this.
*
* @param sit A temporary `SerialIter`; forwarded to `STTx(SerialIter&)`.
* @throws std::runtime_error (same conditions as the lvalue overload).
*/
explicit STTx(SerialIter&& sit);
/** Promote a generic `STObject` to a fully typed transaction.
*
* Used when a transaction arrives as a raw parsed object (e.g., from
* JSON deserialization) and must be graduated to a fully validated
* `STTx`. No wire-size checks are performed; `applyTemplate` enforces
* field conformance against the registered `SOTemplate` for the
* transaction type.
*
* @param object An rvalue `STObject` that must already contain
* `sfTransactionType`. Consumed by the move.
* @throws std::runtime_error if the transaction type is unregistered
* or if `applyTemplate` rejects the field layout.
*/
explicit STTx(STObject&& object);
/** Constructs a transaction.
The returned transaction will have the specified type and
any fields that the callback function adds to the object
that's passed in.
*/
/** Programmatically construct a transaction of the given type.
*
* Installs the `SOTemplate` for `type` and sets `sfTransactionType`,
* then invokes `assembler` to populate remaining fields. After the
* assembler returns, the transaction ID is computed and cached.
*
* @param type The transaction type; must be registered in
* `TxFormats`.
* @param assembler A callable invoked with a mutable reference to the
* newly templated `STObject`. Must not mutate `sfTransactionType`.
* @throws std::runtime_error if `type` is not registered.
* @note Fires `logicError` (not a thrown exception) if `assembler`
* mutates `sfTransactionType` — this is a programming error, not a
* data error.
*/
STTx(TxType type, std::function<void(STObject&)> assembler);
// STObject functions.
/** @return The serialized type ID `STI_TRANSACTION`. */
SerializedTypeID
getSType() const override;
/** @return A human-readable string of the form `"<txid>" = { ... }`. */
std::string
getFullText() const override;
// Outer transaction functions / signature functions.
/** Extract the raw `sfTxnSignature` bytes from an arbitrary object.
*
* @param sigObject The object to read `sfTxnSignature` from; typically
* `*this` or a multi-sign signer sub-object.
* @return The signature bytes, or an empty `Blob` if the field is absent
* or an exception occurs during field access.
*/
static Blob
getSignature(STObject const& sigObject);
/** Extract the `sfTxnSignature` bytes from this transaction. */
Blob
getSignature() const
{
return getSignature(*this);
}
/** Compute the single-sign hash of this transaction.
*
* Prepends `HashPrefix::TxSign` to the serialized form (without signing
* fields) and returns the SHA-512 half-hash. Use this to verify a
* signature without calling `checkSign`.
*
* @return The 256-bit signing hash.
*/
uint256
getSigningHash() const;
/** @return The decoded transaction type, cached at construction time. */
TxType
getTxnType() const;
/** @return The raw bytes of `sfSigningPubKey`. Empty for multi-signed transactions. */
Blob
getSigningPubKey() const;
/** Return a unified sequence proxy abstracting classic sequence and ticket modes.
*
* When `sfSequence` is non-zero the transaction uses classic sequence
* ordering and a `SeqProxy::Sequence` is returned. When `sfSequence` is
* zero and `sfTicketSequence` is present, a `SeqProxy::Ticket` is returned.
* Sequence-type proxies always sort before ticket-type proxies, which the
* protocol relies on for correct processing order.
*
* @return A `SeqProxy` of type `Sequence` or `Ticket` as appropriate.
*/
SeqProxy
getSeqProxy() const;
@@ -83,44 +192,150 @@ public:
std::uint32_t
getSeqValue() const;
/** Resolve the account whose balance pays the transaction fee.
*
* Returns `sfDelegate` if present, otherwise `sfAccount`. Authorization
* of the delegate relationship is enforced separately in the transactor
* layer; this method performs no validation.
*
* @return The `AccountID` of the fee-paying account.
*/
AccountID
getFeePayer() const;
/** Collect every `AccountID` referenced by top-level fields of this transaction.
*
* Walks top-level `STAccount` fields and non-XRP `STAmount` issuers.
* Used to determine which accounts are touched by a transaction for
* indexing and fee purposes.
*
* @return A flat, sorted set of all referenced account IDs.
* @note Only top-level fields are examined; nested objects (e.g.,
* inner multi-sign signers) are not descended into.
*/
boost::container::flat_set<AccountID>
getMentionedAccounts() const;
/** @return The cached transaction ID, computed at construction time. */
uint256
getTransactionID() const;
/** Return the transaction as a JSON object, optionally including the hash.
*
* Includes the `"hash"` key unless `options` has
* `JsonOptions::DisableApiPriorV2` set (API v2+).
*
* @param options JSON rendering options.
* @return A `json::Value` object representing the transaction.
*/
json::Value
getJson(JsonOptions options) const override;
/** Return the transaction as JSON, with an optional binary representation.
*
* When `binary` is `true`, the transaction body is hex-encoded. Under
* API v1 the result wraps the hex in `{"tx": "...", "hash": "..."}`;
* under API v2+ it returns the raw hex string. When `binary` is `false`,
* behaves identically to `getJson(options)`.
*
* @param options JSON rendering options controlling API version behavior.
* @param binary If `true`, serialize the transaction to hex instead of
* expanding fields into JSON.
* @return A `json::Value` containing the transaction representation.
*/
json::Value
getJson(JsonOptions options, bool binary) const;
/** Sign this transaction with the given key pair.
*
* Computes the single-sign payload (hash-prefix + transaction body
* without signing fields), signs it, and writes the signature and public
* key into the transaction. The cached transaction ID is recomputed
* after the signature is stored.
*
* @param publicKey The signer's public key; written to
* `sfSigningPubKey`.
* @param secretKey The corresponding secret key used to produce
* the signature.
* @param signatureTarget If set, the signature is written into that
* named sub-object field (e.g., `sfCounterpartySignature`) instead
* of the transaction root. Used for two-party protocols such as
* `LoanSet`.
*/
void
sign(
PublicKey const& publicKey,
SecretKey const& secretKey,
std::optional<std::reference_wrapper<SField const>> signatureTarget = {});
/** Check the signature.
@param rules The current ledger rules.
@return `true` if valid signature. If invalid, the error message string.
*/
/** Verify the primary signature and, if present, the counterparty signature.
*
* Dispatches to single-sign or multi-sign verification based on whether
* `sfSigningPubKey` is empty. If `sfCounterpartySignature` is present,
* it is verified with the same dispatch; errors from the counterparty
* check are prefixed with `"Counterparty: "`.
*
* @param rules The current ledger rules.
* @return An empty `Expected` on success, or an error string on failure.
*/
Expected<void, std::string>
checkSign(Rules const& rules) const;
/** Verify all batch-signing signatures on a `ttBATCH` transaction.
*
* Iterates over `sfBatchSigners`, dispatching each entry to single- or
* multi-sign batch verification. The signed payload is the output of
* `serializeBatch()` — a batch-specific hash prefix, the outer
* transaction's flags, and the IDs of the inner transactions — which
* binds each signer to the exact set of inner transactions.
*
* @param rules The current ledger rules.
* @return An empty `Expected` on success, or an error string on failure.
* @note Asserts and returns an error if called on a non-batch transaction.
*/
Expected<void, std::string>
checkBatchSign(Rules const& rules) const;
// SQL Functions with metadata.
/** Return the static SQL `INSERT OR REPLACE INTO Transactions` header.
*
* The returned string is the constant prefix used by `getMetaSQL` to
* build persistence statements for the local SQLite `Transactions` table.
*
* @return A reference to a process-lifetime static string.
*/
static std::string const&
getMetaSQLInsertReplaceHeader();
/** Produce a SQL value tuple for this transaction with `Validated` status.
*
* Serializes the transaction and delegates to the full overload with
* `TxnSql::Validated` as the status code.
*
* @param inLedger The ledger sequence number containing this
* transaction.
* @param escapedMetaData Pre-escaped binary metadata string for the
* `TxnMeta` column.
* @return A SQL value tuple string suitable for appending to
* `getMetaSQLInsertReplaceHeader()`.
*/
std::string
getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const;
/** Produce a SQL value tuple with explicit status and raw transaction bytes.
*
* Formats a parenthesized row for the `Transactions` table containing
* the transaction ID, type name, source account (Base58), sequence
* number, ledger sequence, a single-character status code, the raw
* serialized transaction blob, and pre-escaped metadata.
*
* @param rawTxn The serialized transaction bytes (by value).
* @param inLedger The ledger sequence number containing this
* transaction.
* @param status The persistence status code for the `Status`
* column.
* @param escapedMetaData Pre-escaped binary metadata for `TxnMeta`.
* @return A SQL value tuple string.
*/
std::string
getMetaSQL(
Serializer rawTxn,
@@ -128,54 +343,118 @@ public:
TxnSql status,
std::string const& escapedMetaData) const;
/** Return the cached IDs of the inner transactions in a `ttBATCH` transaction.
*
* On the first call, hashes each entry in `sfRawTransactions` and
* stores the result in `batchTxnIds_`. Subsequent calls return the
* cached vector directly. An assertion on every call verifies that the
* cache size still matches `sfRawTransactions`, enforcing the invariant
* that inner transactions may not be modified after the IDs have been
* observed.
*
* @return A const reference to the cached vector of inner transaction IDs.
* @note Must only be called on a `ttBATCH` transaction with a non-empty
* `sfRawTransactions` array.
*/
std::vector<uint256> const&
getBatchTransactionIDs() const;
private:
/** Check the signature.
@param rules The current ledger rules.
@param sigObject Reference to object that contains the signature fields.
Will be *this more often than not.
@return `true` if valid signature. If invalid, the error message string.
*/
/** Dispatch to single- or multi-sign verification for an arbitrary object.
*
* Inspects `sfSigningPubKey` in `sigObject`: empty → multi-sign path,
* non-empty → single-sign path.
*
* @param rules The current ledger rules.
* @param sigObject The object carrying signature fields; usually `*this`
* but may be a counterparty sub-object.
* @return An empty `Expected` on success, or an error string on failure.
*/
Expected<void, std::string>
checkSign(Rules const& rules, STObject const& sigObject) const;
/** Verify a single-sign signature against the transaction body. */
Expected<void, std::string>
checkSingleSign(STObject const& sigObject) const;
/** Verify multi-sign signatures against the transaction body. */
Expected<void, std::string>
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
/** Verify a single-sign batch signature for one `sfBatchSigners` entry. */
Expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner) const;
/** Verify multi-sign batch signatures for one `sfBatchSigners` entry. */
Expected<void, std::string>
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
/** Placement-new copy into a pre-allocated buffer; supports `STVar` SOO. */
STBase*
copy(std::size_t n, void* buf) const override;
/** Placement-new move into a pre-allocated buffer; supports `STVar` SOO. */
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
/** Lazily populated cache of inner transaction IDs for `ttBATCH` transactions. */
mutable std::vector<uint256> batchTxnIds_;
};
/** Run all local pre-submission validity checks on a transaction object.
*
* Gate-keeps local relay and submission by enforcing:
* - Memo field size (max 1024 bytes serialized) and RFC 3986 character
* legality for `MemoType`/`MemoFormat`.
* - All `STAccount` fields must carry non-zero (non-default) values.
* - Pseudo-transaction types (`ttAMENDMENT`, `ttFEE`, `ttUNL_MODIFY`) are
* rejected; they are synthesized internally by the ledger.
* - MPT amounts may only appear in fields that explicitly declare MPT support
* via `soeMPTSupported`.
* - Batch inner transactions must not themselves be `ttBATCH`, and the
* `sfRawTransactions` / `sfBatchSigners` arrays must not exceed
* `kMAX_BATCH_TX_COUNT` entries.
*
* This is a free function rather than an `STTx` method because it can run
* on any `STObject` before it is promoted to a full `STTx`.
*
* @param st The transaction object to validate.
* @param reason Populated with a human-readable failure description when
* the function returns `false`.
* @return `true` if all checks pass; `false` on the first failure.
*/
bool
passesLocalChecks(STObject const& st, std::string&);
passesLocalChecks(STObject const& st, std::string& reason);
/** Sterilize a transaction.
The transaction is serialized and then deserialized,
ensuring that all equivalent transactions are in canonical
form. This also ensures that program metadata such as
the transaction's digest, are all computed.
*/
/** Canonicalize a transaction via a serialize-then-deserialize round trip.
*
* Serializes `stx` to bytes, then constructs a fresh `STTx` from those bytes
* via `SerialIter`. The result is in wire-canonical form: all equivalent
* in-memory representations collapse to the same byte sequence, field
* ordering is normalized, and the transaction ID is freshly computed.
*
* Any code that synthesizes a transaction from JSON or via the programmatic
* assembler constructor and then submits it to the consensus pipeline should
* call `sterilize` first.
*
* @param stx The source transaction to sterilize.
* @return A `shared_ptr` to the newly constructed canonical `STTx const`.
* @throws std::runtime_error if the round-trip deserialization fails.
*/
std::shared_ptr<STTx const>
sterilize(STTx const& stx);
/** Check whether a transaction is a pseudo-transaction */
/** Determine whether a transaction object is a ledger-generated pseudo-transaction.
*
* Pseudo-transactions (`ttAMENDMENT`, `ttFEE`, `ttUNL_MODIFY`) are
* synthesized internally by the ledger and must never be submitted by
* external clients. `passesLocalChecks` rejects any object for which this
* returns `true`.
*
* @param tx The transaction object to test; need not be a fully constructed
* `STTx`.
* @return `true` if the object carries a pseudo-transaction type.
*/
bool
isPseudoTx(STObject const& tx);

View File

@@ -1,3 +1,19 @@
/** @file
* Defines `STValidation`, the wire-format object for a single ledger
* validation message in the XRPL consensus protocol.
*
* Validators broadcast one of these objects each consensus round to signal
* agreement on a specific closed ledger. Peers deserialize inbound messages
* into `STValidation` instances, verify signatures, and count them toward
* quorum. The class therefore has two distinct construction paths: one for
* creation-and-signing by the local validator, one for deserialization of a
* peer's message. See the two constructors for details.
*
* `STValidation` is owned via `std::shared_ptr` and wrapped by `RCLValidation`
* in the consensus machinery; that adapter provides the concept interface
* expected by the generic quorum-counting engine without coupling this class
* to consensus-specific logic.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,14 +29,45 @@
namespace xrpl {
// Validation flags
// --- Wire flag constants (stored in sfFlags; part of the signed payload) ---
// This is a full (as opposed to a partial) validation
/** Bit flag indicating a full (as opposed to a partial) validation.
*
* A partial validation signals participation in the consensus round without
* fully endorsing a specific ledger hash. Validators set this flag when they
* have applied the consensus transaction set and validated the resulting
* ledger. Read via `isFull()`.
*/
constexpr std::uint32_t kVF_FULL_VALIDATION = 0x00000001;
// The signature is fully canonical
/** Bit flag indicating that the DER-encoded signature uses the low-S canonical form.
*
* XRPL requires low-S ECDSA signatures to prevent signature malleability.
* The signing constructor always sets this flag, and `isValid()` passes it
* to `verifyDigest()` to enforce canonicality on inbound messages. Because
* this value is stored in `sfFlags` inside the signed payload, it cannot be
* toggled without invalidating the signature.
*/
constexpr std::uint32_t kVF_FULLY_CANONICAL_SIG = 0x80000000;
/** Wire-format representation of a ledger validation message in XRPL consensus.
*
* Inherits from `STObject` for typed-field serialization (the same system
* used by transactions and ledger entries) and from `CountedObject` for
* live-instance tracking in a long-running `rippled` process.
*
* The class maintains two separate concepts that must not be conflated:
* - **Validity** (`valid_`): whether the cryptographic signature is correct.
* Lazily evaluated and cached on first call to `isValid()`.
* - **Trust** (`trusted_`): whether the issuing validator is on this node's
* current Unique Node List (UNL). Set via `setTrusted()`/`setUntrusted()`;
* can change at runtime as the UNL evolves.
*
* @note Only `secp256k1` signing keys are accepted. Passing an `Ed25519`
* public key to either constructor throws at construction time.
* @see RCLValidation — the adapter that exposes this object to the generic
* consensus engine.
*/
class STValidation final : public STObject, public CountedObject<STValidation>
{
bool trusted_ = false;
@@ -39,30 +86,65 @@ class STValidation final : public STObject, public CountedObject<STValidation>
NetClock::time_point seenTime_;
public:
/** Construct a STValidation from a peer from serialized data.
@param sit Iterator over serialized data
@param lookupNodeID Invocable with signature
NodeID(PublicKey const&)
used to find the Node ID based on the public key
that signed the validation. For manifest based
validators, this should be the NodeID of the master
public key.
@param checkSignature Whether to verify the data was signed properly
@note Throws if the object is not valid
*/
/** Deserialize a validation received from a peer.
*
* Parses the binary payload via `STObject`, then extracts the signing
* public key from `sfSigningPubKey`. The `lookupNodeID` callable
* translates the ephemeral signing key to the validator's stable master
* `NodeID` (which may differ when the validator has rotated its ephemeral
* key via the manifest mechanism).
*
* @tparam LookupNodeID Callable with signature `NodeID(PublicKey const&)`.
* For manifest-based validators this should resolve to the master key's
* `NodeID`; for static-key validators it is typically
* `calcNodeID(pk)`.
* @param sit Iterator over the raw serialized validation bytes.
* @param lookupNodeID Invocable that maps the signing `PublicKey` to a
* stable `NodeID` used for UNL membership checks.
* @param checkSignature If `true`, verifies the signature immediately and
* throws on failure. Pass `false` to defer verification to the first
* call of `isValid()` (the pattern used by `PeerImp` to avoid
* synchronous cryptographic work on the peer-message path).
* @throws std::runtime_error if the serialized data is malformed, if the
* signing public key is absent or not a `secp256k1` key, or if
* `checkSignature` is `true` and the signature does not verify.
* @note After construction `seenTime_` is zero; callers must call
* `setSeen()` to record local receipt time before storing the object.
*/
template <class LookupNodeID>
STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature);
/** Construct, sign and trust a new STValidation issued by this node.
@param signTime When the validation is signed
@param publicKey The current signing public key
@param secretKey The current signing secret key
@param nodeID ID corresponding to node's public master key
@param f callback function to "fill" the validation with necessary data
*/
/** Construct, sign, and trust a new validation issued by the local node.
*
* Sets mandatory bookkeeping fields (`sfSigningPubKey`, `sfSigningTime`),
* invokes the filler callback `f(*this)` so the caller can attach
* optional fields (ledger hash, consensus hash, fee votes, amendment
* bits, server version), then signs the result with `signDigest` and
* marks the object as trusted. The `kVF_FULLY_CANONICAL_SIG` flag is
* always set, enforcing low-S ECDSA on the produced signature.
*
* After `f` returns a format-validation sweep checks that all
* `SoeRequired` fields are present; a missing required field is a
* programming error and triggers `logicError`.
*
* `seenTime_` is initialized to `signTime`, making sign time and seen
* time identical for locally created validations.
*
* @tparam F Callable with signature `void(STValidation&)`.
* @param signTime The time at which the validation is being signed;
* stored in `sfSigningTime` and used as the initial `seenTime_`.
* @param pk The validator's current ephemeral signing public key.
* Must be a `secp256k1` key; passing any other type calls
* `logicError`.
* @param sk The secret key matching `pk`, used to produce `sfSignature`.
* @param nodeID The stable master-key `NodeID` of this validator.
* @param f Callback invoked after mandatory fields are set but before
* signing. Use it to populate `sfLedgerHash`, `sfLedgerSequence`,
* `sfConsensusHash`, `sfFlags`, and any optional advisory fields.
* @note The resulting object is immediately marked as trusted and
* `valid_` is set to `true` without re-verifying, since the node
* just produced the signature.
*/
template <typename F>
STValidation(
NetClock::time_point signTime,
@@ -71,53 +153,171 @@ public:
NodeID const& nodeID,
F&& f);
// Hash of the validated ledger
/** Return the hash of the ledger this validation endorses.
*
* @return Value of the `sfLedgerHash` field.
*/
uint256
getLedgerHash() const;
// Hash of consensus transaction set used to generate ledger
/** Return the hash of the consensus transaction set that produced the validated ledger.
*
* @return Value of the `sfConsensusHash` field. Returns a zero hash if
* the field is absent (the field is optional in the schema).
*/
uint256
getConsensusHash() const;
/** Return the time at which the validator claims to have signed this validation.
*
* Reads `sfSigningTime` from the serialized payload; because that field
* is part of the signed content it cannot be forged without invalidating
* the signature. Note that this is the validator's own clock time and
* may differ from `getSeenTime()`, which is when the *local* node
* received the message.
*
* @return The signing instant as a `NetClock::time_point`.
*/
NetClock::time_point
getSignTime() const;
/** Return the local time at which this node received or created the validation.
*
* For peer-sourced validations this is set via `setSeen()` after receipt.
* For self-issued validations the constructor initializes it to `signTime`.
* This value is never serialized or sent over the wire.
*
* @return The local receipt time as a `NetClock::time_point`.
*/
NetClock::time_point
getSeenTime() const noexcept;
/** Return the ephemeral public key that signed this validation.
*
* May differ from the validator's stable master key when the validator
* has rotated its signing key via the manifest mechanism.
*
* @return A reference to the immutable `signingPubKey_`.
*/
PublicKey const&
getSignerPublic() const noexcept;
/** Return the stable master-key `NodeID` of the issuing validator.
*
* For manifest-based validators this is derived from the master public
* key rather than the ephemeral signing key. It is the identity used
* for UNL membership checks and quorum counting.
*
* @return A reference to the immutable `nodeID_`.
*/
NodeID const&
getNodeID() const noexcept;
/** Verify the cryptographic signature, caching the result for future calls.
*
* On the first call, verifies the ECDSA signature over `getSigningHash()`
* using `signingPubKey_`. The `kVF_FULLY_CANONICAL_SIG` flag is consulted
* to enforce low-S canonicality. The result is stored in `valid_` and
* returned on all subsequent calls without re-computing.
*
* For self-issued validations the signing constructor pre-sets
* `valid_ = true`, so this method never performs cryptographic work.
*
* @return `true` if the signature is valid; `false` otherwise.
*/
bool
isValid() const noexcept;
/** Return whether this is a full (as opposed to partial) validation.
*
* A full validation endorses a specific ledger hash. A partial validation
* only signals that the validator participated in the round.
*
* @return `true` if the `kVF_FULL_VALIDATION` bit is set in `sfFlags`.
*/
bool
isFull() const noexcept;
/** Return whether this validation is marked as trusted by the local node.
*
* Trust reflects whether the issuing validator is on this node's current
* UNL and is independent of cryptographic validity. Self-issued
* validations are always trusted from construction.
*
* @return The current value of the `trusted_` flag.
*/
bool
isTrusted() const noexcept;
/** Compute the domain-separated hash that was (or will be) signed.
*
* Prepends `HashPrefix::Validation` (`'V','A','L',0x00`) to the canonical
* serialization of all signed fields, then applies SHA-512-Half. The
* prefix prevents a validation hash from colliding with any other signed
* payload type (transactions, proposals, etc.).
*
* @return The 256-bit signing digest.
*/
uint256
getSigningHash() const;
/** Mark this validation as trusted.
*
* Called when the issuing validator is confirmed to be on this node's
* current UNL. May be called multiple times; subsequent calls are no-ops.
*/
void
setTrusted();
/** Mark this validation as untrusted.
*
* Called when the issuing validator is removed from this node's current
* UNL, or when the validation is being re-evaluated. Does not affect the
* cryptographic `valid_` cache.
*/
void
setUntrusted();
/** Record the local time at which this node received the validation.
*
* Should be called immediately after constructing a peer-sourced
* validation, before the object is stored or forwarded. For self-issued
* validations the signing constructor sets this to `signTime`
* automatically.
*
* @param s The local receipt time.
*/
void
setSeen(NetClock::time_point s);
/** Serialize this validation to its complete binary wire format.
*
* The returned bytes include all fields, including `sfSignature`, and are
* suitable for network transmission or deduplication hashing. To suppress
* relay of a duplicate message, callers typically hash this output with
* `sha512Half`.
*
* @return A `Blob` containing the complete serialized validation.
*/
Blob
getSerialized() const;
/** Return the raw DER-encoded ECDSA signature from the serialized payload.
*
* @return Value of the `sfSignature` field as a `Blob`.
*/
Blob
getSignature() const;
/** Produce a human-readable summary of this validation for logging.
*
* Renders all major fields (ledger hash, consensus hash, sign/seen times,
* signer public key, node ID, validity, fullness, trust status, signing
* hash, and Base58-encoded public key) into a single-line string.
*
* @return A diagnostic string; not suitable for machine parsing or
* network transmission.
*/
std::string
render() const
{
@@ -134,6 +334,12 @@ public:
}
private:
/** Return the field schema for `STValidation` objects.
*
* Function-local static to guarantee that all `SField` singletons are
* initialized before the `SOTemplate` is constructed (C++ provides no
* cross-translation-unit initialization order for namespace-scope statics).
*/
static SOTemplate const&
validationFormat();
@@ -168,14 +374,6 @@ STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool ch
XRPL_ASSERT(nodeID_.isNonZero(), "xrpl::STValidation::STValidation(SerialIter) : nonzero node");
}
/** Construct, sign and trust a new STValidation issued by this node.
@param signTime When the validation is signed
@param publicKey The current signing public key
@param secretKey The current signing secret key
@param nodeID ID corresponding to node's public master key
@param f callback function to "fill" the validation with necessary data
*/
template <typename F>
STValidation::STValidation(
NetClock::time_point signTime,

View File

@@ -1,3 +1,11 @@
/** @file
* Declares STVector256, the serialized type for ordered lists of uint256 values.
*
* On the wire the array is encoded as a single VL-prefixed blob of concatenated
* 32-byte hashes (type identifier STI_VECTOR256, code 19). Common ledger fields
* that use this type include sfAmendments, sfIndexes, and sfHashes.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -7,92 +15,268 @@
namespace xrpl {
/** Serialized type for an ordered list of 256-bit hash values.
*
* Wraps a `std::vector<uint256>` with the `STBase` contract so that hash
* collections can be stored as named, typed fields inside `STObject` —
* giving them a wire-format identity (`STI_VECTOR256`, code 19), a
* canonical binary encoding (VL-prefixed blob of packed 32-byte values),
* and a JSON representation (array of hex strings). Typical ledger uses
* include `sfAmendments` (active amendments in a validator vote),
* `sfIndexes` (keys in a `DirectoryNode` page), and `sfHashes`.
*
* An empty `STVector256` is the canonical default state and is omitted from
* the wire encoding when the field is declared optional.
*
* The `CountedObject<STVector256>` mixin adds lock-free instance counting
* for diagnostic purposes, with no overhead in the fast path.
*/
class STVector256 : public STBase, public CountedObject<STVector256>
{
std::vector<uint256> value_;
public:
/** Reference type used when this value is passed as a read-only handle. */
using value_type = std::vector<uint256> const&;
/** Construct an empty, unnamed STVector256. */
STVector256() = default;
/** Construct an empty STVector256 bound to the given field name.
*
* @param n The SField that identifies this field in its parent STObject.
*/
explicit STVector256(SField const& n);
/** Construct an unnamed STVector256 pre-populated with @p vector.
*
* @param vector Initial contents; copied into the internal store.
*/
explicit STVector256(std::vector<uint256> const& vector);
/** Construct an STVector256 bound to @p n and pre-populated with @p vector.
*
* @param n The SField that identifies this field in its parent STObject.
* @param vector Initial contents; copied into the internal store.
*/
STVector256(SField const& n, std::vector<uint256> const& vector);
/** Deserialize an STVector256 from a wire-format stream.
*
* Reads a single VL-prefixed blob from @p sit and partitions it into
* consecutive 32-byte chunks, each becoming one `uint256` entry. The
* resulting vector retains the original wire order.
*
* @param sit Forward-only iterator positioned at the VL length prefix of
* the field. Consumed by exactly one VL-length + slice read pair.
* @param name The SField that identifies this field within its parent STObject.
* @throws std::runtime_error if the decoded blob length is not an exact
* multiple of 32 bytes, indicating corrupt or truncated data.
*/
STVector256(SerialIter& sit, SField const& name);
/** Return the serialized type identifier for this field.
*
* @return `STI_VECTOR256` (code 19).
*/
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Serialize this array into @p s as a VL-prefixed blob.
*
* Writes a length prefix followed by the concatenated raw bytes of each
* `uint256` entry (32 bytes per element, no padding or separators).
*
* @param s Accumulator to append the encoded field into.
* @note Asserts (debug builds only) that the associated SField is marked
* binary and carries type `STI_VECTOR256`. These guards catch accidental
* field-type mismatches before data reaches the wire.
*/
void
add(Serializer& s) const override;
/** Produce a JSON array of hex-encoded hash strings.
*
* Each `uint256` entry is rendered as a lowercase hex string via
* `to_string()`. The @p options parameter is accepted for interface
* conformance but is unused; the representation is identical across
* all API versions.
*
* @return A `json::arrayValue` with one hex string per entry.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Test deep equality with another STBase instance.
*
* Two `STVector256` objects are equivalent when they contain the same
* sequence of `uint256` values in the same order.
*
* @param t The object to compare against.
* @return `true` if @p t is an `STVector256` with identical contents;
* `false` if the types differ or the sequences do not match.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Return whether this object holds no entries.
*
* An empty `STVector256` is the canonical default value. Per XRPL
* serialization rules, default-valued optional fields are omitted from
* the wire encoding and contribute nothing to a transaction or ledger hash.
*
* @return `true` if the internal vector is empty.
*/
[[nodiscard]] bool
isDefault() const override;
/** Replace the contents with a copy of @p v.
*
* @param v Source vector; copied into the internal store.
* @return Reference to this object.
*/
STVector256&
operator=(std::vector<uint256> const& v);
/** Replace the contents by moving @p v into the internal store.
*
* @param v Source vector; left in a valid but unspecified state after the call.
* @return Reference to this object.
*/
STVector256&
operator=(std::vector<uint256>&& v);
/** Copy the inner vector from @p v, leaving the SField name unchanged.
*
* Unlike `operator=`, this copies only the payload (`mValue`), not the
* field binding. Use this when you need to transfer values between two
* fields that have different SField identities.
*
* @param v Source object whose contents are copied.
*/
void
setValue(STVector256 const& v);
/** Retrieve a copy of the vector we contain */
/** Return a copy of the internal vector.
*
* Marked `explicit` to prevent accidental implicit copies in generic
* contexts; prefer `value()` for read-only access.
*/
explicit
operator std::vector<uint256>() const;
/** Return the number of entries in the vector.
*
* @return Entry count; 0 for an empty (default) object.
*/
[[nodiscard]] std::size_t
size() const;
/** Resize the internal vector to @p n entries.
*
* New entries (if any) are value-initialized to the zero `uint256`.
*
* @param n Target size.
*/
void
resize(std::size_t n);
/** Return whether the vector contains no entries.
*
* @return `true` if `size() == 0`.
*/
[[nodiscard]] bool
empty() const;
/** Return a mutable reference to the entry at index @p n.
*
* @param n Zero-based index; behavior is undefined if out of range.
*/
std::vector<uint256>::reference
operator[](std::vector<uint256>::size_type n);
/** Return a read-only reference to the entry at index @p n.
*
* @param n Zero-based index; behavior is undefined if out of range.
*/
std::vector<uint256>::const_reference
operator[](std::vector<uint256>::size_type n) const;
/** Return a read-only reference to the internal vector.
*
* Prefer this over the explicit conversion operator for non-owning access.
*
* @return Const reference to the underlying `std::vector<uint256>`.
*/
[[nodiscard]] std::vector<uint256> const&
value() const;
/** Insert @p value before @p pos.
*
* @param pos Iterator before which the new element is inserted.
* @param value Hash to insert.
* @return Iterator to the inserted element.
*/
std::vector<uint256>::iterator
insert(std::vector<uint256>::const_iterator pos, uint256 const& value);
/** Append @p v to the end of the vector.
*
* @param v Hash to append.
*/
void
pushBack(uint256 const& v);
/** Return a mutable iterator to the first element. */
std::vector<uint256>::iterator
begin();
/** Return a read-only iterator to the first element. */
[[nodiscard]] std::vector<uint256>::const_iterator
begin() const;
/** Return a mutable past-the-end iterator. */
std::vector<uint256>::iterator
end();
/** Return a read-only past-the-end iterator. */
[[nodiscard]] std::vector<uint256>::const_iterator
end() const;
/** Remove the element at @p position.
*
* @param position Iterator to the element to remove.
* @return Iterator to the element following the removed one.
*/
std::vector<uint256>::iterator
erase(std::vector<uint256>::iterator position);
/** Remove all entries, leaving the vector empty. */
void
clear() noexcept;
private:
/** Copy this object into a caller-supplied buffer via the STVar placement protocol.
*
* Called only by `detail::STVar`. Delegates to `STBase::emplace`, which
* constructs in-place when @p buf is large enough, or heap-allocates otherwise.
*
* @param n Size of the buffer at @p buf, in bytes.
* @param buf Destination buffer for placement construction.
* @return Pointer to the newly constructed `STVector256`.
*/
STBase*
copy(std::size_t n, void* buf) const override;
/** Move this object into a caller-supplied buffer via the STVar placement protocol.
*
* Called only by `detail::STVar`. Delegates to `STBase::emplace`, which
* constructs in-place when @p buf is large enough, or heap-allocates otherwise.
* The source is left in a valid but unspecified state.
*
* @param n Size of the buffer at @p buf, in bytes.
* @param buf Destination buffer for placement construction.
* @return Pointer to the newly constructed `STVector256`.
*/
STBase*
move(std::size_t n, void* buf) override;
@@ -132,7 +316,6 @@ STVector256::setValue(STVector256 const& v)
value_ = v.value_;
}
/** Retrieve a copy of the vector we contain */
inline STVector256::
operator std::vector<uint256>() const
{

View File

@@ -10,6 +10,26 @@ namespace xrpl {
class Serializer;
class STObject;
/** Serialized type encoding the four-field specification of an XRPL cross-chain bridge.
*
* A bridge connects two independent ledgers: a *locking chain* (where XRP or
* tokens are held in escrow) and an *issuing chain* (where a wrapped
* representation is minted). Each side is described by a door account
* (`AccountID`) and an asset (`Issue`). This class bundles those four pieces
* — `LockingChainDoor`, `LockingChainIssue`, `IssuingChainDoor`,
* `IssuingChainIssue` — into a single, typed, wire-format ledger field that
* appears in bridge-related transactions and ledger entries.
*
* Inherits `STBase` (type-ID `STI_XCHAIN_BRIDGE`) for polymorphic
* serialization and `CountedObject` for debug instance tracking.
*
* Both `operator==` and `operator<` compare all four fields in declaration
* order via `std::tie`, making `STXChainBridge` usable as a key in ordered
* associative containers.
*
* @see XChainAttestations.h for how bridges are consumed by witness and
* attestation logic.
*/
class STXChainBridge final : public STBase, public CountedObject<STXChainBridge>
{
STAccount lockingChainDoor_{sfLockingChainDoor};
@@ -18,80 +38,240 @@ class STXChainBridge final : public STBase, public CountedObject<STXChainBridge>
STIssue issuingChainIssue_{sfIssuingChainIssue};
public:
/** Self-alias used by template code that calls `.value()` to strip the
* ST wrapper; for compound types the value type is the type itself. */
using value_type = STXChainBridge;
/** Identifies which of the two ledgers in a bridge a given door or asset
* belongs to. */
enum class ChainType { Locking, Issuing };
/** Returns the chain that is opposite to @p ct.
*
* @param ct The chain whose counterpart is requested.
* @return `ChainType::Issuing` when @p ct is `Locking`; `Locking`
* otherwise.
*/
static ChainType
otherChain(ChainType ct);
/** Maps the witness `wasLockingChainSend` flag to the originating chain.
*
* Normalizes a boolean attestation flag into a `ChainType`, removing
* scattered `if (wasLockingChainSend)` branches from callers.
*
* @param wasLockingChainSend `true` when the send originated on the
* locking chain.
* @return `ChainType::Locking` when @p wasLockingChainSend is `true`;
* `ChainType::Issuing` otherwise.
*/
static ChainType
srcChain(bool wasLockingChainSend);
/** Maps the witness `wasLockingChainSend` flag to the destination chain.
*
* Complement of `srcChain()`: returns the chain that receives the assets.
*
* @param wasLockingChainSend `true` when the send originated on the
* locking chain.
* @return `ChainType::Issuing` when @p wasLockingChainSend is `true`;
* `ChainType::Locking` otherwise.
*/
static ChainType
dstChain(bool wasLockingChainSend);
/** Constructs an empty bridge bound to `sfXChainBridge`.
*
* Used as a canonical reference instance (e.g., to obtain the known
* JSON key set for extra-field detection in the JSON constructor).
*/
STXChainBridge();
/** Constructs an empty bridge bound to the given field name.
*
* @param name The `SField` tag to associate with this object inside an
* enclosing `STObject`.
*/
explicit STXChainBridge(SField const& name);
STXChainBridge(STXChainBridge const& rhs) = default;
/** Extracts bridge sub-fields from an already-parsed generic `STObject`.
*
* Used during ledger deserialization when the parent has been parsed as
* an `STObject` and the four bridge fields must be projected into the
* strongly-typed form.
*
* @param o Source object; must contain `sfLockingChainDoor`,
* `sfLockingChainIssue`, `sfIssuingChainDoor`, and
* `sfIssuingChainIssue` — `STObject::operator[]` throws `FieldErr`
* if any field is absent.
*/
STXChainBridge(STObject const& o);
/** Constructs a bridge from its four constituent values.
*
* @param srcChainDoor Door account on the locking chain.
* @param srcChainIssue Asset locked or released on the locking chain.
* @param dstChainDoor Door account on the issuing chain.
* @param dstChainIssue Wrapped asset minted or burned on the issuing chain.
*/
STXChainBridge(
AccountID const& srcChainDoor,
Issue const& srcChainIssue,
AccountID const& dstChainDoor,
Issue const& dstChainIssue);
/** Deserializes a bridge from a JSON object, binding to `sfXChainBridge`.
*
* Delegates to the two-argument form with `sfXChainBridge`.
*
* @param v JSON object with keys `LockingChainDoor`, `LockingChainIssue`,
* `IssuingChainDoor`, `IssuingChainIssue`.
* @throws std::runtime_error if @p v is not an object, contains
* unrecognized keys, or either door field is not a valid Base58-encoded
* account.
*/
explicit STXChainBridge(json::Value const& v);
/** Deserializes a bridge from a JSON object, binding to @p name.
*
* Performs a strict whitelist check against the canonical key set from a
* default-constructed bridge before parsing any values, rejecting typos
* and unknown fields at parse time rather than silently ignoring them.
*
* @param name The `SField` to associate with this object.
* @param v JSON object with the four bridge fields.
* @throws std::runtime_error if @p v is not an object, contains any key
* absent from the canonical set, or either door is not a valid
* Base58-encoded account.
*/
explicit STXChainBridge(SField const& name, json::Value const& v);
/** Deserializes a bridge from a binary stream.
*
* Hot path for on-disk and network deserialization. Reads the four
* sub-fields in canonical order: locking door, locking issue, issuing
* door, issuing issue. Each sub-field consumes its own field-ID header
* and payload bytes from @p sit.
*
* @param sit Forward-only cursor positioned at the first byte of the
* bridge payload; advanced past all four fields on return.
* @param name The `SField` to associate with this object.
*/
explicit STXChainBridge(SerialIter& sit, SField const& name);
STXChainBridge&
operator=(STXChainBridge const& rhs) = default;
/** Returns a human-readable representation of the bridge for diagnostics.
*
* Format: `{ LockingChainDoor = <addr>, LockingChainIssue = <issue>,
* IssuingChainDoor = <addr>, IssuingChainIssue = <issue> }`.
*
* @return Formatted string; intended for logging and debug output only.
*/
[[nodiscard]] std::string
getText() const override;
/** Converts this bridge into a generic `STObject` with the same four fields.
*
* Needed when the bridge must participate in code paths that operate on
* `STObject` graphs, such as transaction metadata construction.
*
* @return A new `STObject` bound to `sfXChainBridge` containing copies of
* all four bridge sub-fields.
*/
[[nodiscard]] STObject
toSTObject() const;
/** Returns the door account of the locking chain. */
[[nodiscard]] AccountID const&
lockingChainDoor() const;
/** Returns the asset locked or released on the locking chain. */
[[nodiscard]] Issue const&
lockingChainIssue() const;
/** Returns the door account of the issuing chain. */
[[nodiscard]] AccountID const&
issuingChainDoor() const;
/** Returns the wrapped asset minted or burned on the issuing chain. */
[[nodiscard]] Issue const&
issuingChainIssue() const;
/** Returns the door account for the specified chain.
*
* Allows generic code (e.g., attestation handlers) to query either side
* of a bridge without hard-coding which chain is locking vs. issuing.
* Pair with `srcChain()`/`dstChain()` to map a `wasLockingChainSend`
* boolean to the correct `ChainType`.
*
* @param ct Which side of the bridge to query.
* @return The locking-chain door when @p ct is `Locking`; the
* issuing-chain door otherwise.
*/
[[nodiscard]] AccountID const&
door(ChainType ct) const;
/** Returns the asset for the specified chain.
*
* @param ct Which side of the bridge to query.
* @return The locking-chain issue when @p ct is `Locking`; the
* issuing-chain issue otherwise.
*/
[[nodiscard]] Issue const&
issue(ChainType ct) const;
/** Returns `STI_XCHAIN_BRIDGE`, the type discriminator for this ST class. */
[[nodiscard]] SerializedTypeID
getSType() const override;
/** Serializes the bridge to JSON.
*
* Produces an object with keys `LockingChainDoor`, `LockingChainIssue`,
* `IssuingChainDoor`, `IssuingChainIssue`. The canonical key set from
* this output is also used by the JSON constructor to detect extra fields.
*
* @return JSON object representation of all four bridge fields.
*/
[[nodiscard]] json::Value getJson(JsonOptions) const override;
/** Appends the binary encoding of all four sub-fields to @p s.
*
* Each sub-field is written in canonical declaration order (locking door,
* locking issue, issuing door, issuing issue) and includes its own
* field-ID header, mirroring the `SerialIter` constructor's read order.
*
* @param s Serializer accumulator to append to.
*/
void
add(Serializer& s) const override;
/** Polymorphic equality check used by `STBase` container comparisons.
*
* Performs a `dynamic_cast` to `STXChainBridge` and delegates to
* `operator==`. Returns `false` if @p t is not an `STXChainBridge`.
*
* @param t The object to compare against.
* @return `true` iff @p t is an `STXChainBridge` with identical fields.
*/
[[nodiscard]] bool
isEquivalent(STBase const& t) const override;
/** Returns `true` when all four sub-fields are in their default state. */
[[nodiscard]] bool
isDefault() const override;
/** Returns a reference to this object itself.
*
* Satisfies the convention that template code calling `.value()` on an
* ST type receives the unwrapped value. For compound types like
* `STXChainBridge`, `value_type` equals the type itself.
*
* @return `*this`.
*/
[[nodiscard]] value_type const&
value() const noexcept;
@@ -111,6 +291,11 @@ private:
operator<(STXChainBridge const& lhs, STXChainBridge const& rhs);
};
/** Returns `true` iff the two bridges have identical door accounts and assets.
*
* Comparison is performed via `std::tie` across all four fields in
* declaration order: locking door, locking issue, issuing door, issuing issue.
*/
inline bool
operator==(STXChainBridge const& lhs, STXChainBridge const& rhs)
{
@@ -126,6 +311,11 @@ operator==(STXChainBridge const& lhs, STXChainBridge const& rhs)
rhs.issuingChainIssue_);
}
/** Strict weak ordering over bridges; enables use as a `std::map`/`std::set` key.
*
* Comparison is performed via `std::tie` across all four fields in
* declaration order: locking door, locking issue, issuing door, issuing issue.
*/
inline bool
operator<(STXChainBridge const& lhs, STXChainBridge const& rhs)
{

View File

@@ -13,10 +13,27 @@
namespace xrpl {
/** A secret key. */
/** A 32-byte private key for either secp256k1 or Ed25519.
*
* The destructor unconditionally zeroes the backing buffer via `secureErase`,
* defending against cold-boot and memory-dump attacks. Intermediate buffers
* in all key-generation and derivation helpers are likewise erased.
*
* Comparison operators are deleted: comparing secret keys in application code
* is almost always a mistake (compare public keys or `AccountID`s instead),
* and any comparison implementation risks timing-observable branches that
* could leak key material through a side channel.
*
* `operator<<` is absent by design — streaming a secret key to a log or debug
* output is too easy an accident. Use `toString()` for the rare legitimate case.
*
* @note The default constructor is deleted; a `SecretKey` must always be
* initialised with actual key material.
*/
class SecretKey
{
public:
/** Size of the raw key buffer in bytes. */
static constexpr std::size_t kSIZE = 32;
private:
@@ -30,54 +47,78 @@ public:
SecretKey&
operator=(SecretKey const&) = default;
/** Deleted: comparing secret keys risks timing side-channel leaks. */
bool
operator==(SecretKey const&) = delete;
/** Deleted: comparing secret keys risks timing side-channel leaks. */
bool
operator!=(SecretKey const&) = delete;
/** Zeroes the key buffer via `secureErase` before releasing memory. */
~SecretKey();
/** Construct from a 32-byte array.
*
* @param data Raw key material; copied into the internal buffer.
*/
SecretKey(std::array<std::uint8_t, kSIZE> const& data);
/** Construct from a `Slice`.
*
* @param slice Raw key material; must be exactly 32 bytes.
* @throws LogicError if `slice.size() != 32`.
*/
SecretKey(Slice const& slice);
/** @return Pointer to the first byte of the raw 32-byte key material. */
[[nodiscard]] std::uint8_t const*
data() const
{
return buf_;
}
/** @return Number of bytes in the key buffer (always 32). */
[[nodiscard]] std::size_t
size() const
{
return sizeof(buf_);
}
/** Convert the secret key to a hexadecimal string.
@note The operator<< function is deliberately omitted
to avoid accidental exposure of secret key material.
*/
/** Return the key as a hexadecimal string.
*
* Use this only where the hex representation is genuinely required
* (e.g. CLI tooling). Prefer keeping the key in its binary form
* everywhere else. `operator<<` is intentionally absent to prevent
* accidental exposure in log output.
*
* @return Hex-encoded string of the 32-byte key.
*/
[[nodiscard]] std::string
toString() const;
/** @return Iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
begin() const noexcept
{
return buf_;
}
/** @return Iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
cbegin() const noexcept
{
return buf_;
}
/** @return Past-the-end iterator for the key buffer. */
[[nodiscard]] const_iterator
end() const noexcept
{
return buf_ + sizeof(buf_);
}
/** @return Past-the-end iterator for the key buffer. */
[[nodiscard]] const_iterator
cend() const noexcept
{
@@ -85,61 +126,157 @@ public:
}
};
/** Deleted: comparing secret keys risks timing side-channel leaks. */
bool
operator==(SecretKey const& lhs, SecretKey const& rhs) = delete;
/** Deleted: comparing secret keys risks timing side-channel leaks. */
bool
operator!=(SecretKey const& lhs, SecretKey const& rhs) = delete;
//------------------------------------------------------------------------------
/** Parse a secret key */
/** Decode a Base58Check-encoded secret key.
*
* Decodes the token and validates that the payload is exactly 32 bytes.
* Never throws — returns `std::nullopt` on decoding failure or length
* mismatch.
*
* @param type The expected `TokenType` prefix (e.g. `TokenType::FamilySeed`).
* @param s Base58Check-encoded string to decode.
* @return The decoded `SecretKey`, or `std::nullopt` on any error.
*/
template <>
std::optional<SecretKey>
parseBase58(TokenType type, std::string const& s);
/** Encode a secret key as a Base58Check string.
*
* The `TokenType` argument controls the version byte prepended during
* encoding, consistent with the XRPL token system (e.g. `TokenType::FamilySeed`).
*
* @param type Version byte selector for the Base58Check envelope.
* @param sk The secret key to encode.
* @return Base58Check-encoded string.
*/
inline std::string
toBase58(TokenType type, SecretKey const& sk)
{
return encodeBase58Token(type, sk.data(), sk.size());
}
/** Create a secret key using secure random numbers. */
/** Generate a secret key from the platform CSPRNG.
*
* Fills 32 bytes from `crypto_prng()`, constructs the key, then immediately
* erases the temporary stack buffer. The result is not tied to any seed and
* cannot be deterministically reproduced — use `generateKeyPair` when wallet
* recovery is required.
*
* @return A freshly generated `SecretKey` backed by cryptographically secure
* random bytes.
*/
SecretKey
randomSecretKey();
/** Generate a new secret key deterministically. */
/** Derive a secret key deterministically from a seed.
*
* - **Ed25519**: the secret key is `sha512Half(seed)` directly.
* - **secp256k1**: hashes `seed || counter` with SHA512-Half, retrying with
* an incrementing counter until the result is a valid curve scalar. In
* practice this loop almost never executes more than once.
*
* All intermediate key-material buffers are erased before return.
*
* @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`).
* @param seed The 128-bit XRPL seed.
* @return The derived `SecretKey`.
* @throws std::runtime_error (secp256k1 only) if no valid scalar is found
* within 128 attempts (statistically negligible).
*/
SecretKey
generateSecretKey(KeyType type, Seed const& seed);
/** Derive the public key from a secret key. */
/** Derive the public key corresponding to a secret key.
*
* - **secp256k1**: produces a 33-byte compressed curve point.
* - **Ed25519**: produces a 33-byte key where `buf[0] == 0xED` followed by
* the 32-byte Edwards-curve public key. The `0xED` prefix is the XRPL
* wire convention that `publicKeyType()` uses to distinguish Ed25519 keys
* from secp256k1 keys (which start with `0x02` or `0x03`).
*
* @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`).
* @param sk The secret key to derive from.
* @return The corresponding `PublicKey`.
*/
PublicKey
derivePublicKey(KeyType type, SecretKey const& sk);
/** Generate a key pair deterministically.
This algorithm is specific to the XRPL:
For secp256k1 key pairs, the seed is converted
to a Generator and used to compute the key pair
corresponding to ordinal 0 for the generator.
*/
/** Generate a key pair deterministically from a seed.
*
* This is the main entry point for wallet-style key derivation.
*
* - **secp256k1**: uses XRPL's custom two-level derivation algorithm (which
* predates BIP-32). A root private key is derived from the seed, its
* compressed public key becomes the "generator point", and the child key
* at ordinal 0 is produced by tweaking the root with a SHA512-Half of
* the generator concatenated with the ordinal. Third-party wallets that
* need to import existing XRPL accounts should support this algorithm.
* - **Ed25519**: equivalent to calling `generateSecretKey` then
* `derivePublicKey` directly; no generator indirection is used.
*
* @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`).
* @param seed The 128-bit XRPL seed.
* @return `{PublicKey, SecretKey}` pair.
* @throws std::runtime_error propagated from secp256k1 root-key derivation
* if no valid scalar is found within 128 attempts (statistically
* negligible).
* @see https://xrpl.org/cryptographic-keys.html#secp256k1-key-derivation
*/
std::pair<PublicKey, SecretKey>
generateKeyPair(KeyType type, Seed const& seed);
/** Create a key pair using secure random numbers. */
/** Generate a key pair from the platform CSPRNG (non-deterministic).
*
* Combines `randomSecretKey()` with `derivePublicKey()`. Unlike
* `generateKeyPair()`, the result cannot be reproduced from any seed.
* Use this when wallet recovery is not needed.
*
* @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`).
* @return `{PublicKey, SecretKey}` pair backed by random key material.
*/
std::pair<PublicKey, SecretKey>
randomKeyPair(KeyType type);
/** Generate a signature for a message digest.
This can only be used with secp256k1 since Ed25519's
security properties come, in part, from how the message
is hashed.
*/
/** Sign a pre-computed digest with a secp256k1 key.
*
* Restricted to secp256k1: Ed25519's security proof depends on how the
* message is hashed internally by the primitive, so pre-hashed signing is
* not supported for Ed25519. Passes a `LogicError` if `pk` is not a
* secp256k1 key.
*
* The ECDSA nonce is generated deterministically per RFC 6979, eliminating
* the class of vulnerabilities caused by weak random nonces. The result is
* DER-encoded and at most 72 bytes.
*
* @param pk Public key; must be secp256k1 (used to verify the key type).
* @param sk Corresponding secret key.
* @param digest The 32-byte SHA512-Half digest to sign.
* @return DER-encoded signature in a `Buffer` (up to 72 bytes).
*/
/** @{ */
Buffer
signDigest(PublicKey const& pk, SecretKey const& sk, uint256 const& digest);
/** Sign a pre-computed digest, deriving the public key from `type` and `sk`.
*
* Convenience overload that calls `derivePublicKey(type, sk)` internally.
* Restricted to secp256k1 — see the primary overload for details.
*
* @param type Must be `KeyType::Secp256k1`.
* @param sk The secret key to sign with.
* @param digest The 32-byte SHA512-Half digest to sign.
* @return DER-encoded signature in a `Buffer` (up to 72 bytes).
*/
inline Buffer
signDigest(KeyType type, SecretKey const& sk, uint256 const& digest)
{
@@ -147,14 +284,35 @@ signDigest(KeyType type, SecretKey const& sk, uint256 const& digest)
}
/** @} */
/** Generate a signature for a message.
With secp256k1 signatures, the data is first hashed with
SHA512-Half, and the resulting digest is signed.
*/
/** Sign a raw message with a key of the detected type.
*
* Dispatches on the key type embedded in `pk`:
* - **Ed25519**: passes the raw message bytes directly to `ed25519_sign`,
* which incorporates its own deterministic internal hashing. Returns a
* fixed 64-byte signature.
* - **secp256k1**: applies SHA512-Half to the message then signs the
* resulting digest with RFC 6979 deterministic nonces. Returns a
* DER-encoded signature of up to 72 bytes.
*
* @param pk Public key; its type determines the signing algorithm.
* @param sk Corresponding secret key.
* @param message Raw message bytes to sign.
* @return Signature in a `Buffer` (64 bytes for Ed25519, ≤72 for secp256k1).
*/
/** @{ */
Buffer
sign(PublicKey const& pk, SecretKey const& sk, Slice const& message);
/** Sign a raw message, deriving the public key from `type` and `sk`.
*
* Convenience overload that calls `derivePublicKey(type, sk)` internally.
* See the primary overload for signing semantics.
*
* @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`).
* @param sk The secret key to sign with.
* @param message Raw message bytes to sign.
* @return Signature in a `Buffer` (64 bytes for Ed25519, ≤72 for secp256k1).
*/
inline Buffer
sign(KeyType type, SecretKey const& sk, Slice const& message)
{

View File

@@ -9,7 +9,30 @@
namespace xrpl {
/** Seeds are used to generate deterministic secret keys. */
/** A 128-bit secret seed from which all XRPL key material is derived.
*
* A `Seed` is the root secret in the XRPL key hierarchy. From a single seed,
* a deterministic derivation produces the private key, public key, and account
* address. The class enforces two security invariants:
*
* - **No default construction.** A zero-initialized seed could be mistaken
* for valid entropy, so every `Seed` must be explicitly constructed from
* real material.
* - **Secure destruction.** The destructor calls `secure_erase()` on the
* internal buffer to overwrite key material in heap/stack memory before
* the object is released. CPU caches and registers may still retain
* remnants; this is a best-effort measure consistent with industry practice.
*
* Copy construction and assignment are allowed so seeds can be passed by
* value into key-derivation functions. Callers should minimize the number
* of live copies.
*
* Only `const` iterators and `data()` are exposed, preventing external
* mutation of the raw key material.
*
* @see randomSeed(), generateSeed(), parseGenericSeed(), parseBase58<Seed>()
* @see SecretKey.h for the derivation step that consumes a Seed
*/
class Seed
{
private:
@@ -24,47 +47,60 @@ public:
Seed&
operator=(Seed const&) = default;
/** Destroy the seed.
The buffer will first be securely erased.
*/
/** Destroy the seed, securely erasing the internal buffer first. */
~Seed();
/** Construct a seed */
/** @{ */
/** Construct a seed from a byte slice.
*
* @param slice Raw bytes to copy into the seed buffer.
* @throws LogicError if `slice.size() != 16`.
*/
explicit Seed(Slice const& slice);
explicit Seed(uint128 const& seed);
/** @} */
/** Construct a seed from a 128-bit integer.
*
* @param seed The 128-bit value whose raw bytes are copied into the seed
* buffer.
* @throws LogicError if `seed.size() != 16`.
*/
explicit Seed(uint128 const& seed);
/** Return a pointer to the first byte of the seed buffer. */
[[nodiscard]] std::uint8_t const*
data() const
{
return buf_.data();
}
/** Return the size of the seed buffer in bytes (always 16). */
[[nodiscard]] std::size_t
size() const
{
return buf_.size();
}
/** Return a const iterator to the first byte of the seed buffer. */
[[nodiscard]] const_iterator
begin() const noexcept
{
return buf_.begin();
}
/** Return a const iterator to the first byte of the seed buffer. */
[[nodiscard]] const_iterator
cbegin() const noexcept
{
return buf_.cbegin();
}
/** Return a const iterator past the last byte of the seed buffer. */
[[nodiscard]] const_iterator
end() const noexcept
{
return buf_.end();
}
/** Return a const iterator past the last byte of the seed buffer. */
[[nodiscard]] const_iterator
cend() const noexcept
{
@@ -74,42 +110,107 @@ public:
//------------------------------------------------------------------------------
/** Create a seed using secure random numbers. */
/** Generate a cryptographically secure random seed.
*
* Fills a temporary staging buffer via `beast::rngfill()` backed by the
* global CSPRNG (`crypto_prng()`), constructs the `Seed` from it, and then
* immediately calls `secure_erase()` on the staging buffer before returning.
* The staging buffer is erased explicitly because its stack lifetime would
* otherwise extend past the point where the seed has been captured.
*
* @return A freshly generated, cryptographically random seed.
*/
Seed
randomSeed();
/** Generate a seed deterministically.
The algorithm is specific to the XRPL:
The seed is calculated as the first 128 bits
of the SHA512-Half of the string text excluding
any terminating null.
@note This will not attempt to determine the format of
the string (e.g. hex or base58).
*/
/** Derive a seed deterministically from a passphrase.
*
* Implements the XRPL passphrase-to-seed algorithm: the seed is the first
* 128 bits of SHA-512-Half applied to the raw passphrase bytes (no null
* terminator included). The hasher type used (`sha512_half_hasher_s`)
* securely erases its internal state on destruction.
*
* @param passPhrase Arbitrary string treated as raw bytes; not interpreted
* as hex or Base58.
* @return The deterministic seed for the given passphrase.
* @note To parse a string that might be hex, Base58, RFC1751, or a
* passphrase, use `parseGenericSeed()` instead.
*/
Seed
generateSeed(std::string const& passPhrase);
/** Parse a Base58 encoded string into a seed */
/** Decode a Base58Check-encoded seed string.
*
* Decodes a string carrying the `TokenType::FamilySeed` prefix (the
* well-known "s"-prefixed wallet seed strings). The decoded payload must
* be exactly 16 bytes; any other length yields `std::nullopt`.
*
* @param s A Base58Check-encoded string.
* @return The decoded seed, or `std::nullopt` if the string is empty,
* malformed, has an incorrect checksum, or decodes to a payload of
* the wrong size.
*/
template <>
std::optional<Seed>
parseBase58(std::string const& s);
/** Attempt to parse a string as a seed.
@param str the string to parse
@param rfc1751 true if we should attempt RFC1751 style parsing (deprecated)
* */
/** Parse a string in any recognized seed format.
*
* Attempts each format in order, returning on the first match:
*
* 1. **Rejection guard.** Returns `std::nullopt` if the string successfully
* parses as an `AccountID`, node public key, account public key, node
* private key, or account secret. This prevents accidentally using an
* address or public key as a seed.
* 2. **Empty string.** Returns `std::nullopt`.
* 3. **Hex.** A 32-character hex string is decoded directly as a 128-bit
* seed.
* 4. **Base58 family seed.** Delegates to `parseBase58<Seed>()`.
* 5. **RFC1751 mnemonic** (only when `rfc1751 = true`). A 12-word
* English mnemonic decoded per RFC1751 with XRPL's historical
* byte-reversal convention. Parity errors cause fallthrough to the
* passphrase step rather than returning `std::nullopt`.
* 6. **Passphrase fallback.** Any non-empty string that does not match
* the above is passed to `generateSeed()`. This step always succeeds,
* so a non-empty string that is not a recognized key type will always
* produce a seed.
*
* @param str The string to parse.
* @param rfc1751 When `false`, RFC1751 mnemonic decoding is skipped.
* Pass `false` in contexts where strict format enforcement is required
* (e.g., node identity from the command line).
* @return The parsed seed, or `std::nullopt` if the string is empty or
* was recognized as a non-seed key type.
* @note The passphrase fallback means this function never returns
* `std::nullopt` for a non-empty string unless it matches a
* disallowed key type.
*/
std::optional<Seed>
parseGenericSeed(std::string const& str, bool rfc1751 = true);
/** Encode a Seed in RFC1751 format */
/** Encode a seed as an RFC1751 English mnemonic.
*
* Produces a 12-word phrase using the RFC1751 dictionary. XRPL reverses
* the byte order of the seed before encoding — `parseGenericSeed()`
* applies the same reversal symmetrically when decoding.
*
* @param seed The seed to encode.
* @return A space-separated 12-word RFC1751 mnemonic string.
* @note RFC1751 output is considered deprecated. `parseGenericSeed()`
* accepts it by default for backward compatibility; pass
* `rfc1751 = false` to disable that fallback.
*/
std::string
seedAs1751(Seed const& seed);
/** Format a seed as a Base58 string */
/** Encode a seed as a Base58Check string with the `FamilySeed` token type.
*
* Produces the well-known "s"-prefixed wallet seed strings displayed to
* XRPL users.
*
* @param seed The seed to encode.
* @return The Base58Check-encoded seed string.
*/
inline std::string
toBase58(Seed const& seed)
{

View File

@@ -1,3 +1,8 @@
/**
* @file SeqProxy.h
* @brief Unified sequence/ticket identifier for XRPL transactions.
*/
#pragma once
#include <cstdint>
@@ -5,43 +10,65 @@
namespace xrpl {
/** A type that represents either a sequence value or a ticket value.
We use the value() of a SeqProxy in places where a sequence was used
before. An example of this is the sequence of an Offer stored in the
ledger. We do the same thing with the in-ledger identifier of a
Check, Payment Channel, and Escrow.
Why is this safe? If we use the SeqProxy::value(), how do we know that
each ledger entry will be unique?
There are two components that make this safe:
1. A "TicketCreate" transaction carefully avoids creating a ticket
that corresponds with an already used Sequence or Ticket value.
The transactor does this by referring to the account root's
sequence number. Creating the ticket advances the account root's
sequence number so the same ticket (or sequence) value cannot be
used again.
2. When a "TicketCreate" transaction creates a batch of tickets it advances
the account root sequence to one past the largest created ticket.
Therefore all tickets in a batch other than the first may never have
the same value as a sequence on that same account. And since a ticket
may only be used once there will never be any duplicates within this
account.
*/
/** A type-tagged @c uint32_t that identifies a transaction by either a
* traditional account sequence number or a ticket sequence number.
*
* Before the Tickets feature, every XRPL transaction consumed exactly one
* account sequence number in order, so a plain @c uint32_t was sufficient.
* Tickets allow an account to pre-reserve sequence slots and use them
* out-of-order, which introduces a second namespace of transaction
* identifiers. @c SeqProxy encapsulates the choice in one place so callers
* never need to carry a separate @c bool isTicket flag.
*
* The raw @c value() is used as a ledger-object key for Offers, Checks,
* Payment Channels, and Escrows — the same role a bare sequence number
* played before tickets existed. This is safe because of two invariants
* maintained by the @c TicketCreate transactor:
*
* 1. Every ticket created has a numeric value that falls within the range
* the account root's sequence has already advanced past — so a ticket
* value can never equal any sequence number that will be consumed in the
* future by that account.
* 2. When a batch of tickets is created, the account root's sequence is
* advanced to one past the highest ticket number in the batch, permanently
* retiring all of those values from the sequence namespace.
*
* Together these guarantee that ticket values and sequence values for a
* given account never collide, even when stored without type metadata.
*
* @note The sort order imposed by @c operator< places all sequence-typed
* proxies strictly before all ticket-typed proxies, regardless of
* numeric value. @c CanonicalTXSet relies on this to ensure that
* @c TicketCreate transactions (which carry a sequence number) always
* precede the ticket-consuming transactions they enable during consensus
* replay.
*
* @see STTx::getSeqProxy() — primary production construction site
* @see CanonicalTXSet — uses SeqProxy as the per-account sort key
* @see Indexes::ticketIndex() — uses SeqProxy to derive the ledger-object key
*/
class SeqProxy
{
public:
enum class Type : std::uint8_t { Seq = 0, Ticket };
/** Discriminator indicating whether the proxy holds a sequence or ticket. */
enum class Type : std::uint8_t {
Seq = 0, ///< Traditional account sequence number.
Ticket ///< Ticket sequence number (out-of-order slot).
};
private:
std::uint32_t value_;
Type type_;
public:
/** Construct a SeqProxy with an explicit type and value.
*
* Prefer the @c sequence() factory for the common case. Ticket proxies
* are typically constructed directly: @c SeqProxy{SeqProxy::Type::Ticket, v}.
*
* @param t Whether this proxy represents a sequence or a ticket.
* @param v The numeric value of the sequence or ticket.
*/
constexpr explicit SeqProxy(Type t, std::uint32_t v) : value_{v}, type_{t}
{
}
@@ -51,35 +78,60 @@ public:
SeqProxy&
operator=(SeqProxy const& other) = default;
/** Factory function to return a sequence-based SeqProxy */
/** Create a sequence-typed SeqProxy.
*
* Named factory for the common case. Ticket construction uses the
* explicit constructor directly, making it visibly intentional at each
* call site.
*
* @param v The account sequence number.
* @return A SeqProxy of type @c Type::Seq with value @c v.
*/
static constexpr SeqProxy
sequence(std::uint32_t v)
{
return SeqProxy{Type::Seq, v};
}
/** Return the raw numeric value of this proxy.
*
* Used as a ledger-object key for Offers, Checks, Payment Channels, and
* Escrows. Safe to use without the type tag because the TicketCreate
* invariants guarantee no numeric collision between sequence and ticket
* values for the same account (see class-level documentation).
*
* @return The @c uint32_t sequence or ticket number.
*/
[[nodiscard]] constexpr std::uint32_t
value() const
{
return value_;
}
/** Return @c true if this proxy holds a traditional sequence number. */
[[nodiscard]] constexpr bool
isSeq() const
{
return type_ == Type::Seq;
}
/** Return @c true if this proxy holds a ticket sequence number. */
[[nodiscard]] constexpr bool
isTicket() const
{
return type_ == Type::Ticket;
}
// Occasionally it is convenient to be able to increase the value_
// of a SeqProxy. But it's unusual. So, rather than putting in an
// addition operator, you must invoke the method by name. That makes
// if more difficult to invoke accidentally.
/** Increment the proxy's value in place and return @c *this.
*
* A named method rather than @c operator+= is deliberate: incrementing
* a @c SeqProxy is an unusual operation (currently used only in tests to
* step through a sequence of dummy transactions) and the explicit name
* prevents accidental arithmetic on what is normally a fixed identifier.
*
* @param amount Number of positions to advance the value.
* @return Reference to @c *this after the increment.
*/
SeqProxy&
advanceBy(std::uint32_t amount)
{
@@ -87,16 +139,11 @@ public:
return *this;
}
// Comparison
//
// The comparison is designed specifically so _all_ Sequence
// representations sort in front of Ticket representations. This
// is true even if the Ticket value() is less that the Sequence
// value().
//
// This somewhat surprising sort order has benefits for transaction
// processing. It guarantees that transactions creating Tickets are
// sorted in from of transactions that consume Tickets.
/** Test equality — two proxies are equal only if both type and value match.
*
* A sequence proxy and a ticket proxy with the same numeric value are
* @b not equal.
*/
friend constexpr bool
operator==(SeqProxy lhs, SeqProxy rhs)
{
@@ -105,12 +152,24 @@ public:
return (lhs.value() == rhs.value());
}
/** Test inequality. */
friend constexpr bool
operator!=(SeqProxy lhs, SeqProxy rhs)
{
return !(lhs == rhs);
}
/** Less-than comparison with type-first ordering.
*
* All sequence-typed proxies sort strictly before all ticket-typed
* proxies, regardless of numeric value. Within the same type, proxies
* are ordered numerically. This means even the largest possible sequence
* number (@c UINT32_MAX) sorts before the smallest ticket (@c 0).
*
* @note @c CanonicalTXSet depends on this invariant: it ensures that
* @c TicketCreate transactions (sequence-based) always precede the
* ticket-consuming transactions they enable in consensus ordering.
*/
friend constexpr bool
operator<(SeqProxy lhs, SeqProxy rhs)
{
@@ -119,24 +178,28 @@ public:
return lhs.value() < rhs.value();
}
/** Greater-than comparison. */
friend constexpr bool
operator>(SeqProxy lhs, SeqProxy rhs)
{
return rhs < lhs;
}
/** Greater-than-or-equal comparison. */
friend constexpr bool
operator>=(SeqProxy lhs, SeqProxy rhs)
{
return !(lhs < rhs);
}
/** Less-than-or-equal comparison. */
friend constexpr bool
operator<=(SeqProxy lhs, SeqProxy rhs)
{
return !(lhs > rhs);
}
/** Stream a human-readable representation: @c "sequence N" or @c "ticket N". */
friend std::ostream&
operator<<(std::ostream& os, SeqProxy seqProx)
{

View File

@@ -1,3 +1,12 @@
/** @file
* Defines `Serializer` (write side) and `SerialIter` (read side) — the two
* classes that implement the XRPL canonical binary serialization format.
*
* Every transaction, ledger object, and signed message exchanged across the
* XRP Ledger network is encoded using this format. `Serializer` accumulates
* typed values in big-endian byte order; `SerialIter` consumes the resulting
* byte stream as a forward-only cursor.
*/
#pragma once
#include <xrpl/basics/Blob.h>
@@ -17,6 +26,17 @@
namespace xrpl {
/** Accumulates bytes for XRPL canonical binary serialization (write side).
*
* Every `add*` method appends data in big-endian byte order and returns the
* byte offset at which writing began, allowing callers to locate previously
* written slots for later inspection or patching. The default constructor
* pre-reserves 256 bytes to avoid reallocation on typical transaction sizes.
*
* @note The internal `Blob` (`std::vector<unsigned char>`) storage is
* deprecated. New code should prefer zero-copy patterns built on
* `Slice` and `Buffer` where possible.
*/
class Serializer
{
private:
@@ -24,11 +44,21 @@ private:
Blob data_;
public:
/** Construct a serializer, pre-reserving capacity.
*
* @param n Initial byte capacity to reserve (default 256).
*/
explicit Serializer(int n = 256)
{
data_.reserve(n);
}
/** Construct a serializer pre-populated with a copy of an existing buffer.
*
* @param data Pointer to the source bytes. Must be non-null when
* `size != 0`.
* @param size Number of bytes to copy.
*/
Serializer(void const* data, std::size_t size)
{
data_.resize(size);
@@ -40,18 +70,21 @@ public:
}
}
/** Return a non-owning view of the accumulated bytes. */
[[nodiscard]] Slice
slice() const noexcept
{
return Slice(data_.data(), data_.size());
}
/** Return the number of bytes accumulated so far. */
[[nodiscard]] std::size_t
size() const noexcept
{
return data_.size();
}
/** Return a const pointer to the first accumulated byte. */
[[nodiscard]] void const*
data() const noexcept
{
@@ -59,11 +92,33 @@ public:
}
// assemble functions
/** Append a single byte in big-endian order.
*
* @param i Value to append.
* @return Byte offset at which the value was written.
*/
int
add8(unsigned char i);
/** Append a 16-bit unsigned integer in big-endian byte order.
*
* @param i Value to append.
* @return Byte offset at which the value was written.
*/
int
add16(std::uint16_t i);
/** Append a 32-bit integer in big-endian byte order.
*
* Accepts any type whose unsigned form is exactly `uint32_t` (i.e.
* `int32_t` or `uint32_t`), preventing accidental narrowing from wider
* types at compile time.
*
* @tparam T An integer type whose unsigned counterpart is `uint32_t`.
* @param i Value to append.
* @return Byte offset at which the value was written.
*/
template <typename T>
requires(std::is_same_v<std::make_unsigned_t<std::remove_cv_t<T>>, std::uint32_t>)
int
@@ -77,9 +132,30 @@ public:
return ret;
}
/** Append a `HashPrefix` domain-separator as a big-endian 32-bit value.
*
* Hash-domain prefixes (e.g. `TXN`, `STX`, `VAL`) are prepended to
* every signable or hashable payload to prevent cross-domain collisions.
* A `static_assert` in the implementation guards that `HashPrefix`'s
* underlying type remains `uint32_t`, which is an invariant of the wire
* format.
*
* @param p The domain-separation prefix to append.
* @return Byte offset at which the prefix was written.
*/
int
add32(HashPrefix p);
/** Append a 64-bit integer in big-endian byte order.
*
* Accepts any type whose unsigned form is exactly `uint64_t` (i.e.
* `int64_t` or `uint64_t`), preventing accidental narrowing at compile
* time.
*
* @tparam T An integer type whose unsigned counterpart is `uint64_t`.
* @param i Value to append.
* @return Byte offset at which the value was written.
*/
template <typename T>
requires(std::is_same_v<std::make_unsigned_t<std::remove_cv_t<T>>, std::uint64_t>)
int
@@ -97,9 +173,29 @@ public:
return ret;
}
/** Append an integer of any supported width in big-endian byte order.
*
* Dispatches to `add8`, `add16`, `add32`, or `add64` based on `Integer`.
* Explicit specializations in the `.cpp` cover `unsigned char`,
* `uint16_t`, `uint32_t`, `int32_t`, and `uint64_t`.
*
* @tparam Integer One of the supported integer types listed above.
* @param i Value to append.
* @return Byte offset at which the value was written.
*/
template <typename Integer>
int addInteger(Integer);
/** Append the raw bytes of a fixed-width integer type without any prefix.
*
* Covers `uint128`, `uint160`, `uint192`, `uint256`, and any other
* `BaseUInt` specialization.
*
* @tparam Bits Bit width of the `BaseUInt` type.
* @tparam Tag Distinguishing tag type of the `BaseUInt` specialization.
* @param v Value to append.
* @return Byte offset at which the value was written.
*/
template <std::size_t Bits, class Tag>
int
addBitString(BaseUInt<Bits, Tag> const& v)
@@ -107,29 +203,118 @@ public:
return addRaw(v.data(), v.size());
}
/** Append a raw byte sequence without any length prefix.
*
* @param vector Bytes to append.
* @return Byte offset at which the data was written.
*/
int
addRaw(Blob const& vector);
/** Append the bytes referenced by a `Slice` without any length prefix.
*
* @param slice Non-owning view of bytes to append.
* @return Byte offset at which the data was written.
*/
int
addRaw(Slice slice);
/** Append a raw memory region without any length prefix.
*
* @param ptr Pointer to the first byte to append.
* @param len Number of bytes to copy from `ptr`.
* @return Byte offset at which the data was written.
*/
int
addRaw(void const* ptr, int len);
/** Append all bytes accumulated in another `Serializer` without a length prefix.
*
* @param s Source serializer whose buffer is appended in full.
* @return Byte offset at which the data was written.
*/
int
addRaw(Serializer const& s);
/** Append a variable-length-prefixed blob using XRPL's three-tier VL encoding.
*
* Writes a compact 13 byte length header followed by the raw bytes:
* 0192 bytes use a 1-byte header; 19312,480 use 2 bytes; 12,481918,744
* use 3 bytes.
*
* @param vector Data to append.
* @return Byte offset at which the length header was written.
* @throws std::overflow_error if the data exceeds 918,744 bytes.
*/
int
addVL(Blob const& vector);
/** Append a variable-length-prefixed blob from a `Slice`.
*
* Writes a compact length header then the referenced bytes. An empty
* slice writes the header only (length 0).
*
* @param slice Non-owning view of the data to append.
* @return Byte offset at which the length header was written.
* @throws std::overflow_error if the slice exceeds 918,744 bytes.
*/
int
addVL(Slice const& slice);
/** Append a variable-length-prefixed blob from an iterator range.
*
* Writes the length header for a payload of `len` bytes, then iterates
* `[begin, end)` calling `addRaw` on each element's `.data()`/`.size()`.
* In debug builds an assertion verifies that the total bytes iterated
* equals `len`.
*
* @tparam Iter Forward iterator whose value type exposes `.data()` and
* `.size()`.
* @param begin Start of the range.
* @param end Past-the-end of the range.
* @param len Total byte count of all elements in the range.
* @return Byte offset at which the length header was written.
* @throws std::overflow_error if `len` exceeds 918,744.
*/
template <class Iter>
int
addVL(Iter begin, Iter end, int len);
/** Append a variable-length-prefixed blob from a raw pointer.
*
* Writes a compact length header then `len` bytes from `ptr`. When
* `len == 0` only the header is written; `ptr` is not dereferenced.
*
* @param ptr Pointer to the data to append. May be null when `len == 0`.
* @param len Number of bytes to copy.
* @return Byte offset at which the length header was written.
* @throws std::overflow_error if `len` exceeds 918,744.
*/
int
addVL(void const* ptr, int len);
// disassemble functions
bool
get8(int&, int offset) const;
/** Read a single byte at a given offset without consuming it.
*
* @param[out] i Output parameter set to the byte value on success.
* @param offset Zero-based byte offset into the internal buffer.
* @return `true` if `offset` is within bounds; `false` otherwise.
*/
bool
get8(int& i, int offset) const;
/** Read an integer of any supported width from the given byte offset.
*
* Assembles the value from big-endian bytes without consuming them.
*
* @tparam Integer Target integer type; must fit within the buffer from
* `offset`.
* @param[out] number Set to the decoded value on success.
* @param offset Zero-based byte offset at which to start reading.
* @return `true` if `[offset, offset + sizeof(Integer))` is within
* bounds; `false` otherwise (and `number` is unmodified).
*/
template <typename Integer>
bool
getInteger(Integer& number, int offset)
@@ -149,6 +334,19 @@ public:
return true;
}
/** Copy a fixed-width integer type out of the buffer at the given offset.
*
* Uses `memcpy` directly into the `BaseUInt` storage; no byte-order
* conversion is performed, so the buffer must already contain the value
* in the expected byte order.
*
* @tparam Bits Bit width of the `BaseUInt` type.
* @tparam Tag Distinguishing tag type of the `BaseUInt` specialization.
* @param[out] data Destination for the extracted value.
* @param offset Zero-based byte offset at which to start reading.
* @return `true` if `[offset, offset + Bits/8)` is within bounds;
* `false` otherwise (and `data` is unmodified).
*/
template <std::size_t Bits, typename Tag = void>
bool
getBitString(BaseUInt<Bits, Tag>& data, int offset) const
@@ -159,132 +357,268 @@ public:
return success;
}
/** Append a compact TLV field tag used by `STObject` serialization.
*
* Encodes the (type, name) pair into 1, 2, or 3 bytes:
* - Both < 16: one byte `(type << 4) | name`.
* - Type < 16, name ≥ 16: two bytes — `(type << 4)` then `name`.
* - Type ≥ 16, name < 16: two bytes — `name` then `type`.
* - Both ≥ 16: three bytes — `0x00` sentinel, then `type`, then `name`.
*
* @param type Serialized-type family code (1255).
* @param name Per-type field index (1255).
* @return Byte offset at which the tag was written.
* @note Both `type` and `name` must be in [1, 255]; an assertion fires in
* debug builds if either is out of range.
*/
int
addFieldID(int type, int name);
/** Append a field tag using the `SerializedTypeID` enum as the type code.
*
* Convenience overload that casts `type` to `int` before delegating to
* `addFieldID(int, int)`.
*
* @param type Serialized-type family.
* @param name Per-type field index (1255).
* @return Byte offset at which the tag was written.
*/
int
addFieldID(SerializedTypeID type, int name)
{
return addFieldID(safeCast<int>(type), name);
}
/** @deprecated Use `sha512Half(s.slice())` directly instead.
*
* Compute the XRPL "SHA-512 half" hash over the accumulated buffer.
*
* @return The first 256 bits of SHA-512 applied to the accumulated bytes.
*/
// DEPRECATED
[[nodiscard]] uint256
getSHA512Half() const;
// totality functions
/** Return a const reference to the underlying byte vector.
*
* @note The `Blob` type is deprecated; prefer `slice()` for new code.
*/
[[nodiscard]] Blob const&
peekData() const
{
return data_;
}
/** Return a copy of the accumulated byte vector.
*
* @note Allocates; prefer `slice()` to avoid the copy.
*/
[[nodiscard]] Blob
getData() const
{
return data_;
}
/** Return a mutable reference to the underlying byte vector.
*
* Intended for legacy callers that need to splice or overwrite bytes
* in place. New code should not use this.
*/
Blob&
modData()
{
return data_;
}
/** Return the number of accumulated bytes.
*
* @note Prefer `size()` for new code.
*/
[[nodiscard]] int
getDataLength() const
{
return data_.size();
}
/** Return a const pointer to the first accumulated byte. */
[[nodiscard]] void const*
getDataPtr() const
{
return data_.data();
}
/** Return a mutable pointer to the first accumulated byte. */
void*
getDataPtr()
{
return data_.data();
}
/** Return the number of accumulated bytes.
*
* @note Alias for `getDataLength()`; prefer `size()` for new code.
*/
[[nodiscard]] int
getLength() const
{
return data_.size();
}
/** Return the accumulated bytes as a `std::string`. */
[[nodiscard]] std::string
getString() const
{
return std::string(static_cast<char const*>(getDataPtr()), size());
}
/** Clear all accumulated bytes, leaving the buffer empty. */
void
erase()
{
data_.clear();
}
/** Remove bytes from the end of the buffer.
*
* @param num Number of bytes to remove.
* @return `true` on success; `false` if `num` exceeds the current size,
* leaving the buffer unchanged.
*/
bool
chop(int num);
// vector-like functions
Blob ::iterator
/** Return a mutable iterator to the first byte. */
Blob::iterator
begin()
{
return data_.begin();
}
Blob ::iterator
/** Return a mutable past-the-end iterator. */
Blob::iterator
end()
{
return data_.end();
}
[[nodiscard]] Blob ::const_iterator
/** Return a const iterator to the first byte. */
[[nodiscard]] Blob::const_iterator
begin() const
{
return data_.begin();
}
[[nodiscard]] Blob ::const_iterator
/** Return a const past-the-end iterator. */
[[nodiscard]] Blob::const_iterator
end() const
{
return data_.end();
}
/** Reserve capacity for at least `n` bytes without changing the size.
*
* @param n Minimum byte capacity to reserve.
*/
void
reserve(size_t n)
{
data_.reserve(n);
}
/** Resize the buffer to exactly `n` bytes.
*
* New bytes are zero-initialized; existing bytes beyond `n` are dropped.
*
* @param n Target size in bytes.
*/
void
resize(size_t n)
{
data_.resize(n);
}
/** Return the number of bytes that can be held without reallocation. */
[[nodiscard]] size_t
capacity() const
{
return data_.capacity();
}
/** Compare the accumulated bytes against a raw `Blob` for equality. */
bool
operator==(Blob const& v) const
{
return v == data_;
}
/** Compare the accumulated bytes against a raw `Blob` for inequality. */
bool
operator!=(Blob const& v) const
{
return v != data_;
}
/** Compare two `Serializer` instances for byte-for-byte equality. */
bool
operator==(Serializer const& v) const
{
return v.data_ == data_;
}
/** Compare two `Serializer` instances for byte-for-byte inequality. */
bool
operator!=(Serializer const& v) const
{
return v.data_ != data_;
}
/** Return the number of header bytes used to encode a VL prefix.
*
* Dispatches on the first header byte: ≤192 → 1 byte; 193240 → 2
* bytes; 241254 → 3 bytes.
*
* @param b1 First byte of the VL header (0254).
* @return 1, 2, or 3.
* @throws std::overflow_error if `b1` is negative or equals 255.
*/
static int
decodeLengthLength(int b1);
/** Decode a one-byte VL length (0192 range).
*
* @param b1 The sole header byte.
* @return The decoded payload length.
* @throws std::overflow_error if `b1` is negative or > 254.
*/
static int
decodeVLLength(int b1);
/** Decode a two-byte VL length (19312,480 range).
*
* Formula: `193 + (b1 - 193) * 256 + b2`.
*
* @param b1 First header byte (193240).
* @param b2 Second header byte.
* @return The decoded payload length.
* @throws std::overflow_error if `b1` is outside [193, 240].
*/
static int
decodeVLLength(int b1, int b2);
/** Decode a three-byte VL length (12,481918,744 range).
*
* Formula: `12481 + (b1 - 241) * 65536 + b2 * 256 + b3`.
*
* @param b1 First header byte (241254).
* @param b2 Second header byte.
* @param b3 Third header byte.
* @return The decoded payload length.
* @throws std::overflow_error if `b1` is outside [241, 254].
*/
static int
decodeVLLength(int b1, int b2, int b3);
@@ -313,8 +647,22 @@ Serializer::addVL(Iter begin, Iter end, int len)
//------------------------------------------------------------------------------
/** Forward-only cursor over an external byte buffer for XRPL deserialization
* (read side).
*
* Stores a pointer into the caller-owned buffer together with `remain_` (bytes
* not yet consumed) and `used_` (bytes consumed). All `get*` methods advance
* the cursor and throw `std::runtime_error` on underflow — error codes are not
* returned; malformed input is treated as an exceptional condition.
*
* The buffer must outlive the iterator; no ownership is taken. `reset()`
* rewinds to the original position in O(1) using `used_` as the rewind delta.
*
* @note This class is deprecated as a direct dependency. New code should
* prefer zero-copy patterns built on `Slice` and `Buffer`. In
* particular, `getSlice()` is preferred over the copying `getRaw()`.
*/
// DEPRECATED
// Transitional adapter to new serialization interfaces
class SerialIter
{
private:
@@ -323,28 +671,53 @@ private:
std::size_t used_ = 0;
public:
/** Construct a cursor over an existing byte buffer.
*
* The iterator does not take ownership; the caller must ensure that
* `data` remains valid for the iterator's lifetime.
*
* @param data Pointer to the first byte of the buffer.
* @param size Total number of bytes available.
*/
SerialIter(void const* data, std::size_t size) noexcept;
/** Construct a cursor from a `Slice`.
*
* @param slice Non-owning view of the buffer to iterate.
*/
SerialIter(Slice const& slice) : SerialIter(slice.data(), slice.size())
{
}
// Infer the size of the data based on the size of the passed array.
/** Construct a cursor from a fixed-size byte array.
*
* The array size is inferred at compile time.
*
* @tparam N Size of the array (must be > 0).
* @param data Reference to the byte array.
*/
template <int N>
explicit SerialIter(std::uint8_t const (&data)[N]) : SerialIter(&data[0], N)
{
static_assert(N > 0, "");
}
/** Return `true` if all bytes have been consumed. */
[[nodiscard]] bool
empty() const noexcept
{
return remain_ == 0;
}
/** Rewind the cursor to the beginning of the buffer.
*
* O(1): uses `used_` as the rewind delta rather than storing a separate
* copy of the original pointer.
*/
void
reset() noexcept;
/** Return the number of bytes not yet consumed. */
[[nodiscard]] int
getBytesLeft() const noexcept
{
@@ -352,76 +725,199 @@ public:
}
// get functions throw on error
/** Consume and return the next byte.
*
* @return The byte at the current cursor position.
* @throws std::runtime_error if the buffer is exhausted.
*/
unsigned char
get8();
/** Consume and decode the next 2 bytes as a big-endian unsigned 16-bit integer.
*
* @return Decoded value.
* @throws std::runtime_error if fewer than 2 bytes remain.
*/
std::uint16_t
get16();
/** Consume and decode the next 4 bytes as a big-endian unsigned 32-bit integer.
*
* Use `geti32()` for signed values.
*
* @return Decoded value.
* @throws std::runtime_error if fewer than 4 bytes remain.
*/
std::uint32_t
get32();
/** Consume and decode the next 4 bytes as a big-endian signed 32-bit integer.
*
* Uses `boost::endian::load_big_s32` to ensure correct two's-complement
* sign extension.
*
* @return Decoded value.
* @throws std::runtime_error if fewer than 4 bytes remain.
*/
std::int32_t
geti32();
/** Consume and decode the next 8 bytes as a big-endian unsigned 64-bit integer.
*
* Use `geti64()` for signed values.
*
* @return Decoded value.
* @throws std::runtime_error if fewer than 8 bytes remain.
*/
std::uint64_t
get64();
/** Consume and decode the next 8 bytes as a big-endian signed 64-bit integer.
*
* Uses `boost::endian::load_big_s64` to ensure correct two's-complement
* sign extension.
*
* @return Decoded value.
* @throws std::runtime_error if fewer than 8 bytes remain.
*/
std::int64_t
geti64();
/** Consume and return the next `Bits/8` bytes as a `BaseUInt<Bits, Tag>`.
*
* Constructs the result via `BaseUInt::fromVoid`, providing zero-copy
* extraction of fixed-width types such as `uint128`, `uint160`, `uint192`,
* and `uint256`.
*
* @tparam Bits Bit width of the target type (must be a multiple of 8).
* @tparam Tag Distinguishing tag type of the `BaseUInt` specialization.
* @return The decoded value.
* @throws std::runtime_error if fewer than `Bits/8` bytes remain.
*/
template <std::size_t Bits, class Tag = void>
BaseUInt<Bits, Tag>
getBitString();
/** Consume and return the next 16 bytes as a `uint128`. */
uint128
get128()
{
return getBitString<128>();
}
/** Consume and return the next 20 bytes as a `uint160`. */
uint160
get160()
{
return getBitString<160>();
}
/** Consume and return the next 24 bytes as a `uint192`. */
uint192
get192()
{
return getBitString<192>();
}
/** Consume and return the next 32 bytes as a `uint256`. */
uint256
get256()
{
return getBitString<256>();
}
/** Decode and consume the next field-ID tag, inverse of `Serializer::addFieldID`.
*
* Reads 13 bytes depending on the packing scheme.
*
* @param[out] type Decoded type family code (≥ 1).
* @param[out] name Decoded per-type field index (≥ 1).
* @throws std::runtime_error if the buffer is exhausted or a decoded
* uncommon code is < 16 (which would be ambiguous with the common
* single-byte encoding).
*/
void
getFieldID(int& type, int& name);
// Returns the size of the VL if the
// next object is a VL. Advances the iterator
// to the beginning of the VL.
/** Decode and consume the variable-length header, returning the payload size.
*
* Reads 13 header bytes and advances the cursor to the first byte of
* the payload.
*
* @return Decoded payload length in bytes.
* @throws std::runtime_error if the buffer is exhausted mid-header.
* @throws std::overflow_error if the first byte is outside the valid range.
*/
int
getVLDataLength();
/** Return a zero-copy view of the next `bytes` bytes and advance the cursor.
*
* The returned `Slice` points directly into the underlying buffer and is
* valid only while that buffer is alive. Prefer this over `getRaw()`
* when an allocation can be avoided.
*
* @param bytes Number of bytes to expose.
* @return A `Slice` referencing the requested region.
* @throws std::runtime_error if `bytes` exceeds the remaining bytes.
*/
Slice
getSlice(std::size_t bytes);
// VFALCO DEPRECATED Returns a copy
/** @deprecated Prefer `getSlice()` to avoid allocation.
*
* Copy `size` bytes from the current position into a new `Blob` and
* advance the cursor.
*
* @param size Number of bytes to copy.
* @return A `Blob` containing the copied bytes.
* @throws std::runtime_error if `size` exceeds the remaining bytes.
*/
Blob
getRaw(int size);
// VFALCO DEPRECATED Returns a copy
/** @deprecated Prefer `getVLBuffer()` or `getVLDataLength()` + `getSlice()`.
*
* Decode the VL header and return a copy of the payload as a `Blob`.
*
* @return A `Blob` containing the VL payload.
* @throws std::runtime_error if the buffer is exhausted.
*/
Blob
getVL();
/** Advance the cursor by `num` bytes without reading the data.
*
* @param num Number of bytes to skip.
* @throws std::runtime_error if `num` exceeds the remaining bytes.
*/
void
skip(int num);
/** Decode the VL header and return the payload as a move-only `Buffer`.
*
* Equivalent to `getVL()` but avoids the SSO overhead of `std::vector`.
* Prefer this over `getVL()` for new callers.
*
* @return A `Buffer` containing the VL payload.
* @throws std::runtime_error if the buffer is exhausted.
*/
Buffer
getVLBuffer();
/** Copy `size` bytes from the current position into a new container of
* type `T` and advance the cursor.
*
* `T` must be either `Blob` or `Buffer`. The `size == 0` guard skips
* `memcpy` because passing a null pointer — which an empty `Buffer` may
* have — to `memcpy` with a zero count is undefined behavior in C++.
*
* @tparam T Either `Blob` or `Buffer`.
* @param size Number of bytes to copy.
* @return A freshly allocated container holding the copied bytes.
* @throws std::runtime_error if `size` exceeds the remaining bytes.
*/
template <class T>
T
getRawHelper(int size);

View File

@@ -1,3 +1,19 @@
/** @file
* Signing and verification API for XRPL serialized protocol objects.
*
* Every function here follows the same pipeline: prepend the 4-byte
* `HashPrefix` domain-separation constant, serialize the object via
* `STObject::addWithoutSigningFields()` (which omits signature-carrying
* fields to break the circular dependency), then delegate to the raw
* cryptographic primitives in `SecretKey.h` and `PublicKey.h`.
*
* The `HashPrefix` guarantees that a valid signature in one protocol
* context (e.g. a single-signed transaction via `HashPrefix::TxSign`)
* cannot be replayed as a valid signature in another (e.g. a ledger
* validation via `HashPrefix::Validation`), even if both objects happen
* to share identical serialized bytes.
*/
#pragma once
#include <xrpl/protocol/HashPrefix.h>
@@ -7,17 +23,32 @@
namespace xrpl {
/** Sign an STObject
@param st Object to sign
@param prefix Prefix to insert before serialized object when hashing
@param type Signing key type used to derive public key
@param sk Signing secret key
@param sigField Field in which to store the signature on the object.
If not specified the value defaults to `sfSignature`.
@note If a signature already exists, it is overwritten.
*/
/** Sign an STObject and store the resulting signature in the object.
*
* Serializes `st` via `addWithoutSigningFields()` (excluding all
* signing-related fields to avoid circularity), prepends `prefix` as a
* 4-byte domain-separation constant, then computes an asymmetric signature
* over the resulting bytes using `type` and `sk`. The produced signature is
* written into `st` at `sigField`, overwriting any pre-existing value.
*
* @param st The object to sign. Modified in place: `sigField` is set
* to the computed signature.
* @param prefix Domain-separation prefix prepended to the serialized
* payload before hashing. Must match the prefix used by callers of
* `verify()` for the same signing context (e.g. `HashPrefix::TxSign`
* for single-signed transactions, `HashPrefix::Manifest` for validator
* manifests).
* @param type Key algorithm (`secp256k1` or `ed25519`) used to sign.
* Must be consistent with the algorithm of `sk`.
* @param sk Secret key used to compute the signature. The key material
* is never copied or retained beyond the duration of this call.
* @param sigField Field in `st` that receives the signature blob. Defaults
* to `sfSignature` for standard single-signed transactions; pass an
* alternative field (e.g. `sfMasterSignature`) for other signing
* contexts such as validator manifests.
*
* @note Any existing value in `sigField` is unconditionally overwritten.
*/
void
sign(
STObject& st,
@@ -26,14 +57,23 @@ sign(
SecretKey const& sk,
SF_VL const& sigField = sfSignature);
/** Returns `true` if STObject contains valid signature
@param st Signed object
@param prefix Prefix inserted before serialized object when hashing
@param pk Public key for verifying signature
@param sigField Object's field containing the signature.
If not specified the value defaults to `sfSignature`.
*/
/** Verify that an STObject carries a valid signature.
*
* Reads the signature blob from `sigField`, regenerates the identical
* serialized payload used by `sign()` (prefix prepended to
* `addWithoutSigningFields()` output), and verifies the blob against `pk`.
*
* @param st The signed object to verify.
* @param prefix Domain-separation prefix that was prepended during signing.
* Must be the same value that was passed to `sign()`.
* @param pk Public key corresponding to the secret key used to sign.
* @param sigField Field in `st` from which to read the signature blob.
* Defaults to `sfSignature`; pass an alternative field (e.g.
* `sfMasterSignature`) to verify other signing contexts.
* @return `true` if the signature in `sigField` is cryptographically valid
* for the serialized payload and `pk`; `false` if `sigField` is absent
* or the signature does not verify.
*/
bool
verify(
STObject const& st,
@@ -41,25 +81,62 @@ verify(
PublicKey const& pk,
SF_VL const& sigField = sfSignature);
/** Return a Serializer suitable for computing a multisigning TxnSignature. */
/** Build the complete multi-signing payload for a single signer.
*
* Prepends `HashPrefix::TxMultiSign`, serializes `obj` without signing
* fields, then appends `signingID` as a raw 160-bit account identifier.
* The result is equivalent to calling `startMultiSigningData` followed
* immediately by `finishMultiSigningData`.
*
* The `signingID` **must** be incorporated in the payload. Without it an
* attacker could substitute one signer slot for another account that shares
* the same `RegularKey` — a realistic threat when a custodial service
* provides a single signing key across many accounts. Binding the account
* identity into the signed data makes each authorization cryptographically
* specific to that signer slot.
*
* Use this function for single-signer contexts. For batch multi-sig
* verification, prefer `startMultiSigningData` + `finishMultiSigningData`
* to avoid redundant serialization of the shared transaction body.
*
* @param obj The transaction or object being authorized.
* @param signingID The `AccountID` of the signer authorizing `obj`.
* @return A `Serializer` containing the complete signing payload, ready
* for hashing and signing.
* @see startMultiSigningData, finishMultiSigningData
*/
Serializer
buildMultiSigningData(STObject const& obj, AccountID const& signingID);
/** Break the multi-signing hash computation into 2 parts for optimization.
We can optimize verifying multiple multisignatures by splitting the
data building into two parts;
o A large part that is shared by all of the computations.
o A small part that is unique to each signer in the multisignature.
The following methods support that optimization:
1. startMultiSigningData provides the large part which can be shared.
2. finishMultiSigningData caps the passed in serializer with each
signer's unique data.
*/
/** Build the shared prefix of a multi-signing payload.
*
* Prepends `HashPrefix::TxMultiSign` and serializes `obj` without signing
* fields. The returned `Serializer` is identical for every signer of the
* same transaction; pass it to `finishMultiSigningData` once per signer to
* append only the small, signer-specific `AccountID` tail. This split avoids
* re-serializing the (potentially large) transaction body for each signer
* during batch verification.
*
* @param obj The transaction or object being authorized.
* @return A `Serializer` holding the shared signing prefix. The returned
* value must be completed with `finishMultiSigningData` before use.
* @see finishMultiSigningData, buildMultiSigningData
*/
Serializer
startMultiSigningData(STObject const& obj);
/** Append the per-signer suffix to a multi-signing payload in place.
*
* Writes `signingID` as a raw 160-bit bit-string onto the end of `s`,
* completing the payload started by `startMultiSigningData`. After this
* call, `s.slice()` is ready to be passed to the cryptographic sign or
* verify functions.
*
* @param signingID The `AccountID` of the signer being authorized.
* @param s The in-progress `Serializer` returned by
* `startMultiSigningData`. Modified in place.
* @see startMultiSigningData, buildMultiSigningData
*/
inline void
finishMultiSigningData(AccountID const& signingID, Serializer& s)
{

View File

@@ -1,3 +1,13 @@
/** @file
* Protocol-wide constants and validation helpers for the XRP Ledger.
*
* This header is intentionally lightweight: it is included across virtually
* the entire codebase, so it avoids heavy dependencies. Everything here is
* either a fixed property of the XRP Ledger network (total supply, earliest
* known ledger, governance thresholds) or a small convenience function built
* directly on those values.
*/
#pragma once
#include <xrpl/basics/chrono.h>
@@ -8,9 +18,14 @@
namespace xrpl {
// Various protocol and system specific constant globals.
/* The name of the system. */
/** Return the canonical name of the XRP Ledger daemon.
*
* Uses a Meyers singleton (function-local `static`) to avoid the static
* initialization order fiasco. The `inline` specifier allows inclusion in
* multiple translation units without ODR violations.
*
* @return The string `"xrpld"`.
*/
static inline std::string const&
systemName()
{
@@ -18,29 +33,14 @@ systemName()
return kNAME;
}
/** Configure the native currency. */
/** Number of drops in the genesis account. */
constexpr XRPAmount kINITIAL_XRP{100'000'000'000 * kDROPS_PER_XRP};
static_assert(kINITIAL_XRP.drops() == 100'000'000'000'000'000);
static_assert(Number::kMAX_REP >= kINITIAL_XRP.drops());
/** Returns true if the amount does not exceed the initial XRP in existence. */
inline bool
isLegalAmount(XRPAmount const& amount)
{
return amount <= kINITIAL_XRP;
}
/** Returns true if the absolute value of the amount does not exceed the initial
* XRP in existence. */
inline bool
isLegalAmountSigned(XRPAmount const& amount)
{
return amount >= -kINITIAL_XRP && amount <= kINITIAL_XRP;
}
/* The currency code for the native currency. */
/** Return the ISO currency code for the native asset.
*
* Uses a Meyers singleton (function-local `static`) for the same reasons as
* `systemName()`. Callers should prefer this over scattering `"XRP"` literals
* throughout the codebase.
*
* @return The string `"XRP"`.
*/
static inline std::string const&
systemCurrencyCode()
{
@@ -48,20 +48,97 @@ systemCurrencyCode()
return kCODE;
}
/** The XRP ledger network's earliest allowed sequence */
/** Total XRP supply at ledger genesis: 100 billion XRP expressed in drops.
*
* Computed as `100'000'000'000 * kDROPS_PER_XRP` (= 10^17 drops).
* Two `static_assert`s immediately below guard that the raw bit value is
* correct and that `Number::kMAX_REP` can represent it — a compile-time
* tripwire if either the XRP total or `Number`'s internal representation
* is ever changed.
*/
constexpr XRPAmount kINITIAL_XRP{100'000'000'000 * kDROPS_PER_XRP};
static_assert(kINITIAL_XRP.drops() == 100'000'000'000'000'000);
static_assert(Number::kMAX_REP >= kINITIAL_XRP.drops());
/** Return whether @p amount is within the legal unsigned XRP range.
*
* An amount is legal when it does not exceed the total XRP ever in existence.
* Called by `Transactor::preflight1` to reject fee fields that would exceed
* `kINITIAL_XRP`, and by `InvariantCheck` as a post-transaction guard.
*
* @param amount The drop amount to validate.
* @return `true` if `amount <= kINITIAL_XRP`.
*/
inline bool
isLegalAmount(XRPAmount const& amount)
{
return amount <= kINITIAL_XRP;
}
/** Return whether @p amount is within the legal signed XRP range.
*
* Extends `isLegalAmount` to accept negative values, which arise in delta
* and fee calculations. Used by `InvariantCheck` to ensure no ledger
* operation manufactures XRP out of thin air.
*
* @param amount The signed drop amount to validate.
* @return `true` if `amount` is in `[-kINITIAL_XRP, kINITIAL_XRP]`.
*/
inline bool
isLegalAmountSigned(XRPAmount const& amount)
{
return amount >= -kINITIAL_XRP && amount <= kINITIAL_XRP;
}
/** Earliest ledger sequence available on the XRP Ledger mainnet.
*
* Ledgers 132569 were lost in an early network incident and no longer exist
* anywhere. The `Database` class uses this value as the default lower bound
* for the `earliest_seq` configuration parameter, causing any node without
* a custom setting to refuse requests for pre-genesis sequences.
*/
static constexpr std::uint32_t kXRP_LEDGER_EARLIEST_SEQ{32570u};
/** The XRP Ledger mainnet's earliest ledger with a FeeSettings object. Only
* used in asserts and tests. */
/** Earliest mainnet ledger sequence that contains a `FeeSettings` object.
*
* Used exclusively in `XRPL_ASSERT` calls and tests in the form:
* @code
* XRPL_ASSERT(
* ledger->header().seq < kXRP_LEDGER_EARLIEST_FEES ||
* ledger->read(keylet::fees()),
* "...");
* @endcode
* This allows the `FeeSettings` invariant to be checked on modern ledgers
* while skipping it for historical replay of early mainnet ledgers where
* the object did not yet exist.
*/
static constexpr std::uint32_t kXRP_LEDGER_EARLIEST_FEES{562177u};
/** The minimum amount of support an amendment should have. */
/** Required validator support for an amendment to achieve majority.
*
* Represents 80% as a `std::ratio` rather than a floating-point constant
* so that `AmendmentTable` can compute the threshold count with pure integer
* arithmetic (`(trustedValidations * num) / den`), avoiding rounding error
* in this consensus-critical gate.
*/
constexpr std::ratio<80, 100> kAMENDMENT_MAJORITY_CALC_THRESHOLD;
/** The minimum amount of time an amendment must hold a majority */
/** Minimum continuous duration that an amendment must hold 80% validator
* support before it activates on mainnet.
*
* Seeds `Config::AMENDMENT_MAJORITY_TIME`, which operators may override.
* Uses the `weeks` alias from `xrpl/basics/chrono.h` (predates C++20
* `std::chrono::weeks`).
*/
constexpr std::chrono::seconds const kDEFAULT_AMENDMENT_MAJORITY_TIME = weeks{2};
} // namespace xrpl
/** Default peer port (IANA registered) */
/** IANA-registered port for XRP Ledger peer-to-peer connections.
*
* Declared outside the `xrpl` namespace so that networking code constructing
* socket addresses can reference it without namespace qualification.
* Used by `OverlayImpl` peer discovery and the `peer_connect` RPC handler
* as the fallback when no explicit port is configured.
*/
inline std::uint16_t constexpr kDEFAULT_PEER_PORT{2459};

View File

@@ -1,3 +1,10 @@
/** @file
* Declares `AMMOffer`, the synthetic offer adapter that presents an AMM pool
* as a `TOffer`-compatible object for `BookStep`'s generic payment-engine loop.
*
* @see AMMLiquidity, BookStep, QualityFunction
*/
#pragma once
#include <xrpl/ledger/ApplyView.h>
@@ -13,91 +20,264 @@ template <typename TIn, typename TOut>
class AMMLiquidity;
class QualityFunction;
/** Represents synthetic AMM offer in BookStep. AMMOffer mirrors TOffer
* methods for use in generic BookStep methods. AMMOffer amounts
* are changed indirectly in BookStep limiting steps.
/** Synthetic offer representing AMM pool liquidity inside `BookStep`.
*
* `AMMOffer` exposes the same named interface as `TOffer<TIn, TOut>` —
* `quality()`, `amount()`, `consume()`, `fullyConsumed()`, `limitIn()`,
* `limitOut()`, `send()`, `isFunded()`, `adjustRates()`, `checkInvariant()`
* — so that `BookStep` can treat CLOB and AMM liquidity polymorphically via
* structural duck-typing without virtual dispatch.
*
* An `AMMOffer` is not backed by any ledger entry; `key()` always returns
* `std::nullopt` and pool balance updates happen in `BookStep::consumeOffer()`
* via `accountSend`, not here. Each instance may be consumed at most once
* per payment-engine iteration; `AMMLiquidity::getOffer()` creates a fresh
* one for each iteration.
*
* Behavior diverges between single-path and multi-path modes:
* - **Single-path**: `limitOut`/`limitIn` apply the constant-product swap
* formula against `balances_`; `getQualityFunc()` returns an AMM quality
* function with a nonzero slope encoding the pool curve.
* - **Multi-path**: `limitOut`/`limitIn` scale proportionally to `quality_`
* (like a fixed-rate CLOB offer); `getQualityFunc()` returns a constant
* quality function. This preserves strand quality ordering across
* competing paths.
*
* @tparam TIn Amount type for the input asset (`IOUAmount`, `XRPAmount`,
* or `MPTAmount`).
* @tparam TOut Amount type for the output asset (`IOUAmount`, `XRPAmount`,
* or `MPTAmount`).
*
* @note Explicitly instantiated for all eight valid `(TIn, TOut)` pairings in
* `AMMOffer.cpp`; do not add implicit instantiations elsewhere.
*/
template <StepAmount TIn, StepAmount TOut>
class AMMOffer
{
private:
AMMLiquidity<TIn, TOut> const& ammLiquidity_;
// Initial offer amounts. It is fibonacci seq generated for multi-path.
// If the offer size is set based on the competing CLOB offer then
// the AMM offer size is such that if the offer is consumed then
// the updated AMM pool SP quality is going to be equal to competing
// CLOB offer quality. If there is no competing CLOB offer then
// the initial size is set to in=cMax[Native,Value],balances.out.
// While this is not a "real" offer it simulates the case of
// the swap out of the entire side of the pool, in which case
// the swap in amount is infinite.
/** Synthetic offer size as presented to `BookStep`.
*
* In multi-path mode this is a Fibonacci-sequence-scaled amount so that
* successive iterations probe progressively larger AMM liquidity slices.
* In single-path mode it is either quality-matched to the competing CLOB
* offer (full consumption moves the pool's spot price to that quality) or
* a "max offer" representing 99% of the output-side pool balance when no
* CLOB offer exists.
*/
TAmounts<TIn, TOut> const amounts_;
// Current pool balances.
/** Pool token balances at the moment this offer was generated.
*
* Snapshotted separately from `amounts_` because in single-path mode the
* spot-price quality used as `quality_` can diverge from the raw ratio of
* `amounts_` when the offer is sized relative to a competing CLOB.
* `limitOut` and `limitIn` use these in single-path mode to evaluate the
* constant-product swap formula.
*/
TAmounts<TIn, TOut> const balances_;
// The Spot Price quality if balances != amounts
// else the amounts quality
/** Effective exchange-rate quality for this offer.
*
* Equals the spot-price quality derived from `balances_` when
* `balances_ != amounts_` (single-path quality-matched sizing); otherwise
* equals the quality implied directly by `amounts_`.
*/
Quality const quality_;
// AMM offer can be consumed once at a given iteration
/** True once `consume()` has been called; enforces at-most-once crossing. */
bool consumed_{false};
public:
/** Construct from sizing data provided by `AMMLiquidity::getOffer`.
*
* @param ammLiquidity Owning liquidity manager; provides pool metadata
* (account ID, assets, trading fee) and the `AMMContext` that tracks
* cross-iteration state. Must outlive this offer.
* @param amounts Synthetic offer size — Fibonacci-scaled in multi-path
* mode, quality-matched or pool-draining in single-path mode.
* @param balances Live pool balances at the moment of offer generation;
* used by `limitOut`/`limitIn` in single-path mode.
* @param quality Spot-price quality when `balances != amounts`; otherwise
* the quality implied by `amounts`.
*/
AMMOffer(
AMMLiquidity<TIn, TOut> const& ammLiquidity,
TAmounts<TIn, TOut> const& amounts,
TAmounts<TIn, TOut> const& balances,
Quality const& quality);
/** Return the effective exchange-rate quality for this offer.
*
* In single-path mode this is the pool's spot-price quality; in
* multi-path mode it is the fixed quality of the Fibonacci-sized offer.
* `BookStep` uses this to order competing AMM and CLOB offers.
*
* @return The offer's quality (input-to-output ratio, sorted ascending).
*/
[[nodiscard]] Quality
quality() const noexcept
{
return quality_;
}
/** Return the input-side asset of the underlying AMM pool.
*
* @return Reference to the pool's input `Asset`; lifetime is that of
* the owning `AMMLiquidity`.
*/
[[nodiscard]] Asset const&
assetIn() const;
/** Return the output-side asset of the underlying AMM pool.
*
* @return Reference to the pool's output `Asset`; lifetime is that of
* the owning `AMMLiquidity`.
*/
[[nodiscard]] Asset const&
assetOut() const;
/** Return the AMM pool's on-ledger account ID.
*
* `BookStep` uses this as the logical "owner" of the synthetic offer for
* logging and metadata purposes.
*
* @return Reference to the AMM `AccountID`; lifetime is that of the
* owning `AMMLiquidity`.
*/
[[nodiscard]] AccountID const&
owner() const;
/** Return `std::nullopt` to indicate there is no backing ledger entry.
*
* `TOffer::key()` returns the offer's ledger-object key so `BookStep` can
* erase it after crossing. `AMMOffer` has no ledger object; returning
* `nullopt` signals to `BookStep` that no erase is needed.
*
* @return Always `std::nullopt`.
*/
[[nodiscard]] std::optional<uint256>
key() const
{
return std::nullopt;
}
/** Return the synthetic offer size (TakerPays / TakerGets equivalent).
*
* @return Reference to the `{in, out}` amounts set at construction;
* not modified by `limitOut`, `limitIn`, or `consume`.
*/
[[nodiscard]] TAmounts<TIn, TOut> const&
amount() const;
/** Mark this offer as consumed and notify the AMM execution context.
*
* Validates that `consumed` does not exceed the initial offer size, sets
* the `consumed_` flag, and calls `AMMContext::setAMMUsed()` so the outer
* payment engine knows AMM liquidity was touched this iteration.
*
* @note The `view` parameter is accepted for interface compatibility with
* `TOffer::consume` but is not used here. Actual pool balance updates
* are performed in `BookStep::consumeOffer()` via `accountSend`, which
* keeps all ledger mutations in one place.
* @note An AMM offer may only be consumed once per payment-engine iteration.
*
* @param view Mutable ledger view (unused; present for interface
* parity with `TOffer::consume`).
* @param consumed The `{in, out}` amounts actually transferred. Must not
* exceed `amounts_` in either dimension.
* @throws std::logic_error if `consumed.in > amounts_.in` or
* `consumed.out > amounts_.out`.
*/
void
consume(ApplyView& view, TAmounts<TIn, TOut> const& consumed);
/** Return `true` once the offer has been consumed this iteration.
*
* Unlike `TOffer::fullyConsumed()`, which tests whether the remaining
* amount has reached zero, this simply reflects the `consumed_` flag set
* by `consume()`. AMM offers are always either fully consumed or not
* consumed at all within a single payment-engine iteration.
*
* @return `true` if `consume()` has been called; `false` otherwise.
*/
[[nodiscard]] bool
fullyConsumed() const
{
return consumed_;
}
/** Limit out of the provided offer. If one-path then swapOut
* using current balances. If multi-path then ceil_out using
* current quality.
/** Resize the offer to deliver at most `limit` units of the output asset.
*
* **Single-path mode**: applies `swapAssetOut(balances_, limit,
* tradingFee())` — the constant-product formula — for an exact result
* along the AMM curve.
*
* **Multi-path mode**: resizes proportionally via
* `quality().ceilOutStrict(offerAmount, limit, roundUp)`, preserving
* strand quality ordering. The taker overpays slightly, ensuring the
* post-trade pool product does not decrease.
*
* @param offerAmount Current offer size (used for proportional scaling in
* multi-path mode; ignored in single-path mode).
* @param limit Maximum output amount that may be delivered.
* @param roundUp Whether to round the computed input side up (forwarded
* to `ceilOutStrict` in multi-path mode).
* @return Resized `{in, out}` pair where `out <= limit`.
*/
[[nodiscard]] TAmounts<TIn, TOut>
limitOut(TAmounts<TIn, TOut> const& offerAmount, TOut const& limit, bool roundUp) const;
/** Limit in of the provided offer. If one-path then swapIn
* using current balances. If multi-path then ceil_in using
* current quality.
/** Resize the offer to consume at most `limit` units of the input asset.
*
* **Single-path mode**: applies `swapAssetIn(balances_, limit,
* tradingFee())` — the constant-product formula — for an exact result.
*
* **Multi-path mode**: resizes proportionally to `quality_`. When the
* `fixReducedOffersV2` amendment is active, uses `ceilInStrict` (removes
* a small rounding slop present in the older `ceilIn`); the older path is
* preserved for replay of historical ledgers where that amendment was
* inactive.
*
* @param offerAmount Current offer size (used for proportional scaling in
* multi-path mode; ignored in single-path mode).
* @param limit Maximum input amount the taker will supply.
* @param roundUp Whether to round the computed output side up (forwarded
* to `ceilInStrict` when `fixReducedOffersV2` is active).
* @return Resized `{in, out}` pair where `in <= limit`.
*/
[[nodiscard]] TAmounts<TIn, TOut>
limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const& limit, bool roundUp) const;
/** Return the quality function used by the single-path optimizer.
*
* **Single-path mode**: returns a `QualityFunction` with a nonzero slope
* derived from `balances_` and the trading fee, encoding the AMM curve
* `q(out) = -cfee/poolIn × out + poolOut × cfee/poolIn`. The optimizer
* uses this to solve in closed form for the output amount that satisfies
* a requested quality limit.
*
* **Multi-path mode**: returns a constant `QualityFunction` (slope = 0,
* intercept = `quality_`), identical to a CLOB offer, so that the AMM's
* varying spot price does not disturb relative quality ordering across
* competing strands.
*
* @return A `QualityFunction` encoding the effective exchange rate as a
* linear function of output amount.
*/
[[nodiscard]] QualityFunction
getQualityFunc() const;
/** Send funds without incurring the transfer fee
/** Transfer funds from the AMM pool, waiving the transfer fee.
*
* Delegates to `accountSend` with `WaiveTransferFee::Yes`. AMM swaps on
* Payment transactions are exempt from transfer fees; this is the
* send-side enforcement of that exemption (the rate-side enforcement is
* in `adjustRates()`).
*
* @param args Forwarded verbatim to `accountSend`.
* @return The `TER` result of `accountSend`.
*/
template <typename... Args>
static TER
@@ -107,22 +287,53 @@ public:
std::forward<Args>(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes);
}
/** Return `true` unconditionally — the AMM pool is always its own issuer.
*
* Unlike CLOB offers, which can become unfunded if the owner's balance
* falls, an AMM offer is backed by the pool itself and is never
* underfunded at the time it is generated.
*
* @return Always `true`.
*/
[[nodiscard]] bool
isFunded() const
{
// AMM offer is fully funded by the pool
return true;
}
/** Return adjusted transfer-fee rates, zeroing the output-side rate.
*
* AMM swaps on Payment transactions are exempt from transfer fees on the
* output side. Passing `QUALITY_ONE` for `ofrOutRate` suppresses the
* output-side fee that `BookStep` would otherwise apply. The input-side
* rate is passed through unchanged.
*
* @param ofrInRate Transfer fee rate on the input asset (passed through).
* @param ofrOutRate Transfer fee rate on the output asset (ignored;
* replaced with `QUALITY_ONE`).
* @return `{ofrInRate, QUALITY_ONE}`.
*/
static std::pair<std::uint32_t, std::uint32_t>
adjustRates(std::uint32_t ofrInRate, std::uint32_t ofrOutRate)
{
// AMM doesn't pay transfer fee on Payment tx
return {ofrInRate, QUALITY_ONE};
}
/** Check the new pool product is greater or equal to the old pool
* product or if decreases then within some threshold.
/** Verify the constant-product invariant after offer execution.
*
* Recomputes `k = balances_.in × balances_.out` and the post-trade
* product `k' = (balances_.in + consumed.in) × (balances_.out -
* consumed.out)`. The check passes when `k' >= k` (exact conservation)
* or when the relative decrease is within `1e-7`, a tolerance that absorbs
* finite-precision rounding in the swap formulas without masking genuinely
* broken swaps. Violations are logged at error level; the ledger is not
* aborted.
*
* @param consumed Amounts actually consumed in the trade. Must not
* exceed `amounts_` in either dimension.
* @param j Journal for error-level diagnostics on invariant failure.
* @return `true` if the invariant holds (within tolerance); `false`
* otherwise.
*/
[[nodiscard]] bool
checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const;

View File

@@ -8,10 +8,22 @@ namespace xrpl {
class Logs;
/** Iterates and consumes raw offers in an order book.
Offers are presented from highest quality to lowest quality. This will
return all offers present including missing, invalid, unfunded, etc.
*/
/** Consuming iterator over a DEX order book's quality-sorted offer directories.
*
* Traverses offer directories from best quality (highest exchange rate) to
* worst, calling `offerDelete` on each offer before advancing to the next.
* Every offer is exposed without filtering — missing SLEs, expired offers,
* and unfunded entries are all returned; filtering is the caller's
* responsibility (see `TOfferStreamBase`).
*
* Requires `ApplyView` rather than `ReadView` because `step()` physically
* removes the current offer from the ledger view on each advance.
*
* @note `BookTip` is a *consuming* iterator: each call to `step()` deletes
* the previously yielded offer. Callers that wish to keep an offer must
* act on it before calling `step()` again.
* @see TOfferStreamBase, OfferStream
*/
class BookTip
{
private:
@@ -25,37 +37,80 @@ private:
Quality quality_{};
public:
/** Create the iterator. */
/** Construct an iterator positioned before the first offer in a book.
*
* Derives the key-space range for `book` via `getBookBase` (inclusive
* lower bound, best quality) and `getQualityNext` (exclusive upper
* bound, worst quality). No ledger access occurs until the first call
* to `step()`.
*
* @param view The mutable ledger view used for both directory traversal
* and offer deletion during `step()`.
* @param book The currency/issuer pair identifying the order book to
* iterate.
*/
BookTip(ApplyView& view, Book const& book);
/** Ledger index of the quality-directory that owns the current offer.
*
* The directory key encodes the exchange rate in the `uint256` index
* itself. Valid only after a successful call to `step()`.
*/
[[nodiscard]] uint256 const&
dir() const noexcept
{
return dir_;
}
/** Ledger index of the current offer SLE.
*
* Valid only after a successful call to `step()`.
*/
[[nodiscard]] uint256 const&
index() const noexcept
{
return index_;
}
/** Exchange rate of the current offer's quality directory.
*
* Decoded from the directory's `uint256` key via `getQuality()`.
* Valid only after a successful call to `step()`.
*/
[[nodiscard]] Quality const&
quality() const noexcept
{
return quality_;
}
/** Shared pointer to the current offer's SLE, or `nullptr` if missing.
*
* May be `nullptr` when the offer index exists in a directory but
* the corresponding ledger entry has been removed. Callers must
* null-check before use. Valid only after a successful call to `step()`.
*/
[[nodiscard]] SLE::pointer const&
entry() const noexcept
{
return entry_;
}
/** Erases the current offer and advance to the next offer.
Complexity: Constant
@return `true` if there is a next offer
*/
/** Delete the current offer from the ledger and advance to the next.
*
* On every invocation after the first, removes the previously yielded
* offer via `offerDelete` before searching for the next one. Uses
* `view_.succ(book_, end_)` to locate the next occupied quality
* directory in O(log n) without scanning empty key ranges.
*
* Empty directories (which should never occur in a well-formed ledger)
* are silently skipped rather than treated as fatal errors.
*
* @param j Journal used for diagnostic logging during offer deletion and
* directory traversal.
* @return `true` if a next offer was found and the accessor fields are
* valid; `false` if the book is exhausted.
* @note Complexity is O(log n) in the number of ledger entries per call.
*/
bool
step(beast::Journal j);
};

View File

@@ -1,3 +1,12 @@
/** @file
* Public entry point for the XRP Ledger payment flow engine.
*
* Declares `flow()`, the single function that every Payment transaction,
* offer crossing, and check-cash operation calls to move funds through
* the ledger. The implementation resolves source/destination asset types,
* builds execution strands from the supplied path hints, and dispatches
* to a type-parameterised inner loop in `StrandFlow.h`.
*/
#pragma once
#include <xrpl/protocol/Quality.h>
@@ -7,29 +16,74 @@
namespace xrpl {
namespace path::detail {
/** Diagnostic trace populated by `flow()` during testing.
*
* When a non-null pointer is passed to `flow()`, the inner strand-execution
* loop fills this structure with per-strand, per-step execution traces.
* In production the pointer is null and this path has zero overhead.
*/
struct FlowDebugInfo;
} // namespace path::detail
/**
Make a payment from the src account to the dst account
@param view Trust lines and balances
@param deliver Amount to deliver to the dst account
@param src Account providing input funds for the payment
@param dst Account receiving the payment
@param paths Set of paths to explore for liquidity
@param defaultPaths Include defaultPaths in the path set
@param partialPayment If the payment cannot deliver the entire
requested amount, deliver as much as possible, given the constraints
@param ownerPaysTransferFee If true then owner, not sender, pays fee
@param offerCrossing If Yes or Sell then flow is executing offer crossing, not
payments
@param limitQuality Do not use liquidity below this quality threshold
@param sendMax Do not spend more than this amount
@param j Journal to write journal messages to
@param flowDebugInfo If non-null a pointer to FlowDebugInfo for debugging
@return Actual amount in and out, and the result code
*/
/** Execute a payment through the path-finding and strand-execution engine.
*
* Routes funds from `src` to `dst` using the candidate paths in `paths`
* (and optionally the default path). Strand construction, offer-book
* traversal, AMM liquidity, and trust-line balance updates are all staged
* inside `view`. The sandbox is mutated only when the result is
* `tesSUCCESS`; on any failure the sandbox is left pristine.
*
* Source-asset inference: if `sendMax` is present its asset is used as the
* source asset. Otherwise, for IOU deliveries the source asset adopts `src`
* as its issuer (the "any issuer from src" semantic); for MPT and XRP the
* delivery asset is used directly.
*
* @param view Mutable speculative ledger view. All balance and trust-line
* mutations are staged here. The caller owns the sandbox and decides
* whether to commit the result to the underlying view.
* @param deliver Target amount to deliver to `dst`. Determines the
* destination asset and, when `sendMax` is absent, the source asset.
* @param src Account supplying the input funds.
* @param dst Account receiving the delivered funds.
* @param paths Candidate path hints from the transaction's `sfPaths` field.
* Translated into `Strand` objects via `toStrands()`; an empty set is
* valid when `defaultPaths` is true.
* @param defaultPaths When true, the direct src→dst path is added to the
* strand set even if it does not appear in `paths`.
* @param partialPayment When true, the engine delivers as much as possible
* up to `deliver` rather than failing if the full amount cannot be
* routed. Corresponds to the `tfPartialPayment` transaction flag.
* @param ownerPaysTransferFee When true, IOU transfer fees are charged to
* the offer owner rather than the payment sender. Set for offer
* crossing; clear for normal payments.
* @param offerCrossing Distinguishes operational mode: `No` for Payment
* transactions, `Yes` or `Sell` for offer crossing. Affects fee
* attribution, quality constraints, and offer eligibility within each
* strand step.
* @param limitQuality Optional minimum acceptable exchange rate
* (output/input). Book steps stop consuming liquidity once the best
* available offer quality falls below this threshold. Used to enforce
* the taker's price constraint during offer crossing.
* @param sendMax Optional upper bound on the sender's spend. Its asset
* also drives source-asset inference when present.
* @param domainID Optional domain identifier for domain-scoped order books.
* When set, book lookups are restricted to the specified domain and
* threaded down into every `StrandContext` and `BookStep`.
* @param j Journal for diagnostic logging during strand execution.
* @param flowDebugInfo If non-null, the inner flow template populates this
* structure with per-strand execution traces for testing or diagnostics.
* Null in production; the debug path has zero overhead when null.
* @return A `RippleCalc::Output` containing: `actualAmountIn` (source
* spend), `actualAmountOut` (amount delivered), `removableOffers`
* (unfunded/expired offers discovered during traversal — populated even
* on failure so callers can clean up ledger hygiene), and `result()`
* (the `TER` outcome). On failure, only `removableOffers` and
* `result()` are meaningful; `actualAmountIn`/`Out` may be zero.
* @note If strand construction via `toStrands()` fails, the error `TER` is
* returned immediately and the sandbox is not touched.
* @see path::RippleCalc::rippleCalculate for the older pre-Flow path engine
* that shares the same `Output` type.
*/
path::RippleCalc::Output
flow(
PaymentSandbox& view,

View File

@@ -1,3 +1,10 @@
/** @file
* Defines `TOffer`, the typed CLOB offer wrapper used by the payment-path
* engine to read, limit, and consume Central Limit Order Book entries.
*
* @see AMMOffer, BookStep, TOfferStreamBase
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -15,6 +22,32 @@
namespace xrpl {
/** Typed wrapper around a CLOB offer ledger entry for the payment-path engine.
*
* `TOffer` bridges a raw `SLE` (Shared Ledger Entry) and the generic
* `BookStep` template, providing a clean typed interface for reading offer
* amounts, applying partial fills, and routing funds. Template parameters
* `TIn` and `TOut` — constrained to `XRPAmount`, `IOUAmount`, or `MPTAmount`
* — let a single class body handle every asset-type combination while
* permitting compile-time dispatch where serialization paths differ.
*
* `TOffer` and `AMMOffer` expose the same named interface so that `BookStep`
* can treat CLOB and AMM liquidity polymorphically via structural duck-typing
* without virtual dispatch.
*
* @tparam TIn Amount type for the input (TakerPays) side of the offer.
* Must satisfy the `StepAmount` concept (`XRPAmount`, `IOUAmount`, or
* `MPTAmount`).
* @tparam TOut Amount type for the output (TakerGets) side of the offer.
* Must satisfy the `StepAmount` concept.
*
* @note After construction the object is self-contained — it holds copies of
* all relevant amounts and asset identities extracted from the `SLE`.
* The `SLE` itself is not read again until `consume()` writes back the
* updated amounts.
*
* @see AMMOffer, BookStep, TOfferStreamBase
*/
template <StepAmount TIn, StepAmount TOut>
class TOffer
{
@@ -26,23 +59,47 @@ private:
Asset assetOut_;
TAmounts<TIn, TOut> amounts_{};
/** Write the current `amounts_` back into the underlying `SLE` fields.
*
* Uses `if constexpr` to select between the XRP path (`toSTAmount(amount)`
* with no asset context) and the IOU/MPT path (`toSTAmount(amount, asset_)`)
* at compile time, avoiding runtime polymorphism while sharing the body.
* Called only from `consume()`.
*/
void
setFieldAmounts();
public:
TOffer() = default;
/** Construct from a ledger entry and its pre-computed quality.
*
* Reads `sfTakerPays` and `sfTakerGets` from `entry` and converts them
* to the strongly-typed `TIn`/`TOut` amounts via `toAmount<T>()`. Asset
* identities are captured from the `STAmount::asset()` accessors.
*
* @param entry Shared pointer to the offer's `SLE`. Must not be null.
* @param quality Pre-computed quality for this offer as stored in the
* order book page; not recalculated here.
*/
TOffer(SLE::pointer entry, Quality quality);
/** Returns the quality of the offer.
Conceptually, the quality is the ratio of output to input currency.
The implementation calculates it as the ratio of input to output
currency (so it sorts ascending). The quality is computed at the time
the offer is placed, and never changes for the lifetime of the offer.
This is an important business rule that maintains accuracy when an
offer is partially filled; Subsequent partial fills will use the
original quality.
*/
*
* Conceptually the quality is the ratio of output to input currency.
* Internally it is stored as input-to-output (ascending integer order
* maps to descending quality) so that the order book's sort order is
* stable.
*
* Quality is fixed at the moment the offer is placed and never
* recalculated, even after partial fills. This is a deliberate ledger
* invariant: partial fills reduce only the absolute amounts, leaving the
* exchange rate unchanged and preventing accumulated rounding drift from
* silently worsening the effective rate for later takers.
*
* @return The offer's immutable quality.
*/
[[nodiscard]] Quality
quality() const noexcept
{
@@ -56,16 +113,30 @@ public:
return account_;
}
/** Returns the in and out amounts.
Some or all of the out amount may be unfunded.
*/
/** Returns the remaining in/out amounts for this offer.
*
* The out amount reflects what is recorded in the ledger entry; some or
* all of it may be unfunded if the owner's balance has dropped since the
* offer was placed. `TOfferStreamBase` verifies actual owner funds via
* `ownerFunds_` before crossing.
*
* @return Reference to the `{in, out}` pair; valid for the lifetime of
* this `TOffer`.
*/
[[nodiscard]] TAmounts<TIn, TOut> const&
amount() const
{
return amounts_;
}
/** Returns `true` if no more funds can flow through this offer. */
/** Returns `true` if no more funds can flow through this offer.
*
* The offer is considered fully consumed when either the input or output
* side has reached zero. `BookStep` uses this to decide whether to erase
* the offer from the ledger after crossing.
*
* @return `true` if `amounts_.in <= 0` or `amounts_.out <= 0`.
*/
[[nodiscard]] bool
fullyConsumed() const
{
@@ -76,7 +147,20 @@ public:
return false;
}
/** Adjusts the offer to indicate that we consumed some (or all) of it. */
/** Applies a partial or full consumption to this offer and stages the
* update in the ledger view.
*
* Decrements `amounts_` by `consumed`, writes the updated values back
* into the `SLE` via `setFieldAmounts()`, and calls `view.update(entry_)`
* to stage the change in the `ApplyView`.
*
* @param view Mutable ledger view that will receive the updated SLE.
* @param consumed The `{in, out}` amounts actually transferred. Must
* not exceed the current `amounts_` in either dimension.
* @throws std::logic_error if `consumed.in > amounts_.in` or
* `consumed.out > amounts_.out`. The calling code in `BookStep`
* is expected to clamp consumption first via `limitOut`/`limitIn`.
*/
void
consume(ApplyView& view, TAmounts<TIn, TOut> const& consumed)
{
@@ -91,33 +175,109 @@ public:
view.update(entry_);
}
/** Returns the ledger-object key as a hex string, for logging.
*
* @return String representation of the offer's 256-bit ledger key.
*/
[[nodiscard]] std::string
id() const
{
return to_string(entry_->key());
}
/** Returns the 256-bit ledger key of the underlying offer SLE.
*
* `BookStep` uses this key to erase fully-consumed offers from the ledger.
* Unlike `AMMOffer::key()`, which always returns `std::nullopt`, this
* always returns a value for a valid `TOffer`.
*
* @return The offer's ledger object key.
*/
[[nodiscard]] std::optional<uint256>
key() const
{
return entry_->key();
}
/** Returns the input-side asset of this offer.
*
* @return Reference to the `Asset` captured from `sfTakerPays` at
* construction; valid for the lifetime of this `TOffer`.
*/
[[nodiscard]] Asset const&
assetIn() const;
/** Returns the output-side asset of this offer.
*
* @return Reference to the `Asset` captured from `sfTakerGets` at
* construction; valid for the lifetime of this `TOffer`.
*/
[[nodiscard]] Asset const&
assetOut() const;
/** Clamps the offer to deliver at most `limit` units of the output asset.
*
* Always delegates to `Quality::ceilOutStrict()`, which uses a tighter
* rounding algorithm than the older `ceilOut()` to remove slop that
* could keep offers alive longer than they should be. Unlike `limitIn()`,
* the strict ceiling is unconditional — it was deployed before
* `fixReducedOffersV2` and does not require an amendment gate.
*
* @param offerAmount Current offer size used for proportional scaling.
* @param limit Maximum output amount this offer may deliver.
* @param roundUp Whether to round the computed input side up.
* @return Resized `{in, out}` pair where `out <= limit`.
*/
[[nodiscard]] TAmounts<TIn, TOut>
limitOut(TAmounts<TIn, TOut> const& offerAmount, TOut const& limit, bool roundUp) const;
/** Clamps the offer to consume at most `limit` units of the input asset.
*
* When the `fixReducedOffersV2` amendment is active, delegates to
* `Quality::ceilInStrict()`, which removes a small rounding slop present
* in the older `ceilIn()`. The stricter ceiling changes observable
* transaction outcomes (it can prevent tiny residual amounts from keeping
* an offer alive), so it is gated behind the amendment to preserve replay
* of historical ledgers. Without the amendment, falls back to
* `quality_.ceilIn()`.
*
* @note The asymmetry with `limitOut()` — which is always strict — reflects
* the order in which these fixes were deployed on the network.
*
* @param offerAmount Current offer size used for proportional scaling.
* @param limit Maximum input amount the taker will supply.
* @param roundUp Whether to round the computed output side up (only
* forwarded when `fixReducedOffersV2` is active).
* @return Resized `{in, out}` pair where `in <= limit`.
*/
[[nodiscard]] TAmounts<TIn, TOut>
limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const& limit, bool roundUp) const;
/** Transfers funds from the offer owner, charging the issuer's transfer fee.
*
* Delegates to `accountSend` with `WaiveTransferFee::No`, meaning CLOB
* offer owners pay the output asset issuer's transfer fee on each crossing.
* This is in contrast to `AMMOffer::send()`, which passes
* `WaiveTransferFee::Yes` because AMM pools are exempt from transfer fees
* under the protocol rules.
*
* @param args Arguments forwarded verbatim to `accountSend`.
* @return The `TER` result of `accountSend`.
*/
template <typename... Args>
static TER
send(Args&&... args);
/** Returns `true` when the offer owner is also the output-asset issuer.
*
* An IOU issuer can deliver their own currency without holding a balance,
* so the path engine can bypass the normal `ownerFunds_` balance check for
* such offers. Returns `false` for MPT and XRP output assets because
* issuers have no special delivery privilege for those types.
*
* @return `true` only when `account_ == assetOut_.getIssuer()` and the
* output asset is an `Issue` (IOU); `false` otherwise.
*/
[[nodiscard]] bool
isFunded() const
{
@@ -125,6 +285,18 @@ public:
return account_ == assetOut_.getIssuer() && assetOut_.holds<Issue>();
}
/** Returns the in/out transfer-fee rates unchanged.
*
* CLOB offer owners pay both the input-side and output-side transfer fees,
* so both rates are returned as-is. This is in contrast to
* `AMMOffer::adjustRates()`, which zeroes the output-side rate to
* `QUALITY_ONE` because AMM swaps on Payment transactions are exempt from
* output-side transfer fees.
*
* @param ofrInRate Transfer fee rate on the input asset.
* @param ofrOutRate Transfer fee rate on the output asset.
* @return `{ofrInRate, ofrOutRate}` — both rates unchanged.
*/
static std::pair<std::uint32_t, std::uint32_t>
adjustRates(std::uint32_t ofrInRate, std::uint32_t ofrOutRate)
{
@@ -132,8 +304,22 @@ public:
return {ofrInRate, ofrOutRate};
}
/** Check any required invariant. Limit order book offer
* always returns true.
/** Verifies that the consumed amounts do not exceed the available amounts.
*
* Gated on the `fixAMMv1_3` amendment. For well-behaved callers this is
* always a no-op because `consume()` already enforces the same constraint
* — the check exists so `BookStep::consumeOffer()` can invoke it
* uniformly across both `TOffer` and `AMMOffer` (the AMM version performs
* a far more expensive constant-product pool invariant check).
*
* @note The failure branch is marked `LCOV_EXCL_START`; it is considered
* unreachable under normal test coverage and exists purely as a
* defense-in-depth guard.
*
* @param consumed Amounts actually consumed in the trade.
* @param j Journal for error-level diagnostics on invariant failure.
* @return `true` if `consumed` does not exceed `amounts_` in either
* dimension (or if the amendment is inactive); `false` otherwise.
*/
[[nodiscard]] bool
checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const
@@ -241,6 +427,12 @@ TOffer<TIn, TOut>::assetOut() const
return assetOut_;
}
/** Streams a `TOffer` to an output stream by its ledger-object key string.
*
* @param os The output stream.
* @param offer The offer to stream.
* @return `os` after writing the offer's key string.
*/
template <StepAmount TIn, StepAmount TOut>
inline std::ostream&
operator<<(std::ostream& os, TOffer<TIn, TOut> const& offer)

View File

@@ -1,3 +1,12 @@
/** @file
* Order-book iterator used by the Flow payment engine.
*
* Defines `TOfferStreamBase` and `FlowOfferStream`, which together provide
* validated, quality-ordered traversal of a single order-book leg during
* payment processing. All offer validation, expiry handling, and
* unfunded-offer detection are encapsulated here so that `BookStep` only
* needs to drive the `step()` loop.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -12,10 +21,46 @@
namespace xrpl {
/** Base order-book iterator for the Flow payment engine.
*
* Wraps a `BookTip` cursor and adds per-offer validation: expired offers,
* missing ledger entries, zero-amount offers, deep-frozen trust lines,
* permissioned-DEX domain mismatches, and unfunded-offer detection are all
* handled inside `step()`.
*
* The dual-view design (`view_` / `cancelView_`) is critical for correctness:
* when an owner's balance is zero in `view_` the implementation checks the
* same balance in the pristine `cancelView_`. A zero balance in both views
* means the offer was *already* unfunded before this transaction began and
* must be permanently removed. A zero balance only in `view_` means an
* earlier strand consumed the funds; the offer is skipped but not deleted
* (it may be valid if that strand is rolled back).
*
* @tparam TIn Amount type for the book's input asset (`XRPAmount`,
* `IOUAmount`, or `MPTAmount`).
* @tparam TOut Amount type for the book's output asset.
*
* @note The order and logic of operations inside `step()` are consensus-
* critical. Changing them constitutes a protocol-breaking change.
*
* @see FlowOfferStream, BookTip, TOffer
*/
template <StepAmount TIn, StepAmount TOut>
class TOfferStreamBase
{
public:
/** DoS guard that enforces a maximum number of offers examined per payment.
*
* Each call to `step()` increments an internal counter. Once the counter
* reaches the configured limit the method returns `false`, terminating
* offer iteration. This prevents a pathological order book with many
* tiny or invalid offers from forcing unbounded validator work within a
* single transaction.
*
* In `BookStep`, the limit is `MaxOffersToConsume` and the final count is
* returned to the caller so the engine can track total resource usage
* across all strands.
*/
class StepCounter
{
private:
@@ -24,10 +69,20 @@ public:
beast::Journal j_;
public:
/** Construct a counter with the given step budget.
*
* @param limit Maximum number of offers that may be examined.
* @param j Journal for logging when the limit is exceeded.
*/
StepCounter(std::uint32_t limit, beast::Journal j) : limit_(limit), j_(j)
{
}
/** Increment the step count and check the budget.
*
* @return `true` if the budget has not yet been exhausted;
* `false` once `limit_` offers have been examined.
*/
bool
step()
{
@@ -39,6 +94,8 @@ public:
count_++;
return true;
}
/** Return the number of offers examined so far. */
[[nodiscard]] std::uint32_t
count() const
{
@@ -48,28 +105,83 @@ public:
protected:
beast::Journal const j_;
ApplyView& view_;
ApplyView& cancelView_;
ApplyView& view_; ///< Transactional view accumulating current-payment mutations.
ApplyView& cancelView_; ///< Pristine pre-transaction snapshot used for unfunded-offer detection.
Book book_;
bool validBook_;
NetClock::time_point const expire_;
NetClock::time_point const expire_; ///< Close time of the ledger being built; used for expiry checks.
BookTip tip_;
TOffer<TIn, TOut> offer_;
std::optional<TOut> ownerFunds_;
TOffer<TIn, TOut> offer_; ///< The validated offer at the current position; valid only after a successful `step()`.
std::optional<TOut> ownerFunds_; ///< Cached owner funds for the current offer; set by `step()`, nullopt between iterations.
StepCounter& counter_;
/** Remove a directory entry whose corresponding offer ledger object is missing.
*
* Surgically erases the orphaned index from the directory page's
* `sfIndexes` vector in @p view. This is a best-effort cleanup that
* deliberately avoids `ApplyView::dirRemove` to preserve protocol
* compatibility — using `dirRemove` would alter which ledger entries a
* payment touches, constituting a protocol-breaking change. As a
* consequence, an empty directory page may be left behind in edge cases.
*
* @param view The view to apply the directory patch to; called for both
* `view_` and `cancelView_` when an entry is missing.
*/
void
erase(ApplyView& view);
/** Schedule an offer for permanent removal from the ledger.
*
* Called by `step()` for offers that must be deleted regardless of whether
* the current strand is ultimately committed. The concrete subclass
* decides how to store the index (e.g., `FlowOfferStream` accumulates it
* in `permToRemove_`).
*
* @param offerIndex Ledger key of the offer to be permanently removed.
*/
virtual void
permRmOffer(uint256 const& offerIndex) = 0;
/** Determine whether a partially-funded offer should be removed due to
* quality degradation caused by integer rounding.
*
* Offer quality is frozen at creation time for fairness on partial fills.
* When an owner holds less than the offer's `TakerGets`, the effective
* amounts after rounding can produce a quality *worse* than the stored
* quality, which would misrepresent the offer to later consumers. This
* method detects that condition: if the effective quality is lower than
* the stored quality *and* the effective input amount is at or below
* `minPositiveAmount()`, the offer is considered stale and should be
* purged.
*
* The check is skipped when `TakerGets` is XRP because an XRP-output
* offer can only improve in effective quality (a minimum of 1 drop
* remains deliverable at arbitrarily high quality for any realistic IOU
* input amount).
*
* @tparam TTakerPays Compile-time amount type for TakerPays (input side).
* @tparam TTakerGets Compile-time amount type for TakerGets (output side).
* @return `true` if the offer should be removed; `false` if it is still
* usable.
*/
template <class TTakerPays, class TTakerGets>
requires ValidTaker<TTakerPays, TTakerGets>
[[nodiscard]] bool
shouldRmSmallIncreasedQOffer() const;
public:
/** Construct an offer stream for the given order book.
*
* @param view Mutable ledger view accumulating transaction changes.
* @param cancelView Pristine ledger snapshot preceding this transaction,
* used to distinguish "found unfunded" from "became unfunded" offers.
* @param book The currency-pair order book to iterate.
* @param when Close time of the ledger under construction; offers
* whose `sfExpiration` is ≤ this value are removed.
* @param counter Step-budget guard shared with the caller; iteration
* stops when the budget is exhausted.
* @param journal Logging sink.
*/
TOfferStreamBase(
ApplyView& view,
ApplyView& cancelView,
@@ -80,26 +192,50 @@ public:
virtual ~TOfferStreamBase() = default;
/** Returns the offer at the tip of the order book.
Offers are always presented in decreasing quality.
Only valid if step() returned `true`.
*/
/** Return the offer at the tip of the order book.
*
* Offers are always presented in decreasing quality order.
*
* @note Only valid when the most recent call to `step()` returned `true`.
*/
[[nodiscard]] TOffer<TIn, TOut>&
tip() const
{
return const_cast<TOfferStreamBase*>(this)->offer_;
}
/** Advance to the next valid offer.
This automatically removes:
- Offers with missing ledger entries
- Offers found unfunded
- expired offers
@return `true` if there is a valid offer.
*/
/** Advance to the next valid offer in the order book.
*
* Automatically skips or permanently removes offers that are:
* - Missing their ledger entry (corrupted directory state)
* - Expired (`sfExpiration` ≤ close time)
* - Zero-amount (corrupted offer)
* - Backed by a deep-frozen trust line
* - No longer matching their permissioned DEX domain
* - Found unfunded (owner balance was already zero before this transaction)
* - Stale due to quality degradation from integer rounding
*
* "Found unfunded" offers are permanently scheduled for removal via
* `permRmOffer()`. Offers that *became* unfunded because an earlier
* strand consumed the owner's balance are skipped but not permanently
* removed, since they may be valid if that strand is rolled back.
*
* @return `true` if a valid offer is now available via `tip()`;
* `false` when the book is exhausted or the step budget is exceeded.
*
* @note Modifying the order or logic of the checks in this method
* constitutes a protocol-breaking change.
*/
bool
step();
/** Return the owner's available funds for the current offer's output asset.
*
* Reflects the balance in the in-progress transactional view, capped to
* zero if the trust line is frozen or the holder is unauthorized.
*
* @note Only valid when the most recent call to `step()` returned `true`.
*/
[[nodiscard]] TOut
ownerFunds() const
{
@@ -108,23 +244,24 @@ public:
}
};
/** Presents and consumes the offers in an order book.
The `view_' ` `ApplyView` accumulates changes to the ledger.
The `cancelView_` is used to determine if an offer is found
unfunded or became unfunded.
The `permToRemove` collection identifies offers that should be
removed even if the strand associated with this OfferStream
is not applied.
Certain invalid offers are added to the `permToRemove` collection:
- Offers with missing ledger entries
- Offers that expired
- Offers found unfunded:
An offer is found unfunded when the corresponding balance is zero
and the caller has not modified the balance. This is accomplished
by also looking up the balance in the cancel view.
*/
/** Concrete order-book iterator for the Flow payment engine.
*
* Extends `TOfferStreamBase` with a `permToRemove_` set that accumulates the
* ledger keys of offers that must be erased from the ledger regardless of
* whether the strand that found them is ultimately committed. `BookStep`
* reads this set after the strand completes and applies the removals via
* `ApplyView`.
*
* `boost::container::flat_set` is used because the set is typically small
* (a handful of entries per payment), making cache-friendly sorted-array
* storage faster than a node-based container for both insertion and
* iteration.
*
* @tparam TIn Amount type for the book's input asset.
* @tparam TOut Amount type for the book's output asset.
*
* @see TOfferStreamBase, BookStep
*/
template <StepAmount TIn, StepAmount TOut>
class FlowOfferStream : public TOfferStreamBase<TIn, TOut>
{
@@ -134,13 +271,25 @@ private:
public:
using TOfferStreamBase<TIn, TOut>::TOfferStreamBase;
// The following interface allows offer crossing to permanently
// remove self crossed offers. The motivation is somewhat
// unintuitive. See the discussion in the comments for
// BookOfferCrossingStep::limitSelfCrossQuality().
/** Schedule an offer for permanent ledger removal.
*
* Inserts @p offerIndex into `permToRemove_`. This override also
* supports permanent cleanup of self-crossed offers during
* offer-crossing transactions; see
* `BookOfferCrossingStep::limitSelfCrossQuality()` for the motivation.
*
* @param offerIndex Ledger key of the offer to be permanently removed.
*/
void
permRmOffer(uint256 const& offerIndex) override;
/** Return the set of offer indices scheduled for permanent ledger removal.
*
* `BookStep` applies these removals after the strand completes, even if
* the strand itself was discarded. The set is non-empty only when
* invalid offers (expired, missing, found-unfunded, or self-crossed) were
* encountered during iteration.
*/
[[nodiscard]] boost::container::flat_set<uint256> const&
permToRemove() const
{

View File

@@ -1,3 +1,12 @@
/** @file
* Entry point for the XRPL payment path quality engine.
*
* Declares `RippleCalc`, the adapter that orchestrates a speculative
* payment computation through the trust-line and order-book network.
* The `Payment` transactor is the primary caller; offer-crossing logic
* calls `flow()` directly.
*/
#pragma once
#include <xrpl/core/ServiceRegistry.h>
@@ -15,49 +24,108 @@ namespace detail {
struct FlowDebugInfo;
} // namespace detail
/** RippleCalc calculates the quality of a payment path.
Quality is the amount of input required to produce a given output along a
specified path - another name for this is exchange rate.
*/
/** Computes payment quality across the XRPL trust-line and order-book network.
*
* "Quality" is the effective exchange rate for a payment: the input amount
* required to deliver a given output along a set of candidate paths.
*
* The sole public interface is the static `rippleCalculate()` factory, which
* creates a nested `PaymentSandbox` over the caller's view, delegates to
* `flow()`, and commits the nested sandbox to the caller's view only on
* success. The caller retains full ownership of the outer sandbox and may
* discard it if the enclosing transaction fails.
*
* `RippleCalc` objects are lightweight: they hold only a sandbox reference
* and the `permanentlyUnfundedOffers` set. No heap allocation is incurred
* for the common case.
*/
class RippleCalc
{
public:
/** Behavioral knobs for a single `rippleCalculate()` invocation.
*
* A null `Input` pointer passed to `rippleCalculate()` is valid and
* equivalent to the default-constructed values below.
*/
struct Input
{
explicit Input() = default;
/** Allow delivery of less than the full requested amount.
*
* When true, the engine delivers as much as paths allow rather than
* failing with `tecPATH_PARTIAL` if the full amount cannot be routed.
* Corresponds to the `tfPartialPayment` transaction flag.
*/
bool partialPaymentAllowed = false;
/** Include the implicit direct sender→receiver path.
*
* When true (the default), a direct path is added to the strand set
* even if it is absent from `sfPaths`. Set to false only when the
* caller wants to restrict routing to explicitly provided paths.
*/
bool defaultPathsAllowed = true;
/** Enforce a minimum acceptable exchange rate.
*
* When true and `saMaxAmountReq` is positive, the engine derives a
* quality floor of `Amounts(saMaxAmountReq, saDstAmountReq)` and
* rejects liquidity below that rate. Prevents accepting a worse rate
* than the sender specified via `sfSendMax`.
*/
bool limitQuality = false;
/** Distinguish open-ledger from closed-ledger processing.
*
* Passed through to downstream rounding and fee logic. Set to
* `view().open()` by the `Payment` transactor.
*/
bool isLedgerOpen = true;
};
/** Results of a `rippleCalculate()` or `flow()` invocation. */
struct Output
{
explicit Output() = default;
// The computed input amount.
/** Actual source amount consumed from the sender's account. */
STAmount actualAmountIn;
// The computed output amount.
/** Actual amount delivered to the destination account. */
STAmount actualAmountOut;
// Collection of offers found expired or unfunded. When a payment
// succeeds, unfunded and expired offers are removed. When a payment
// fails, they are not removed. This vector contains the offers that
// could have been removed but were not because the payment fails. It is
// useful for offer crossing, which does remove the offers.
/** Offers found to be expired or unfunded during path traversal.
*
* On a successful payment these offers are deleted from the ledger as
* a side effect. On failure the ledger is not modified, but this set
* is still populated so that offer-crossing logic can clean up stale
* state independently of payment outcome.
*
* The flat_set provides compact, ordered storage for deterministic
* iteration — consensus requires every validator to process the same
* offers in the same sequence.
*/
boost::container::flat_set<uint256> removableOffers;
private:
TER calculationResult_ = temUNKNOWN;
public:
/** Return the TER outcome of the calculation. */
[[nodiscard]] TER
result() const
{
return calculationResult_;
}
/** Set the TER outcome of the calculation.
*
* Controlled access prevents callers from accidentally overwriting
* an internally-set error code.
*
* @param value The TER code to store.
*/
void
setResult(TER const value)
{
@@ -65,43 +133,70 @@ public:
}
};
/** Compute the quality of a payment and stage ledger mutations speculatively.
*
* Creates a nested `PaymentSandbox` over `view`, invokes `flow()` to walk
* paths and cross order books, then — on success — commits the nested
* sandbox back into `view`. On any failure `view` is left unmodified.
*
* Any exception thrown by `flow()` is caught and converted to a
* `tecINTERNAL` result so the transaction is stored rather than silently
* dropped by the node.
*
* @param view Caller-owned speculative ledger view. Mutations are staged
* here only when the return value carries `tesSUCCESS`. The caller
* decides whether to commit `view` to the underlying ledger.
* @param saMaxAmountReq Maximum amount the sender is willing to spend
* (`sfSendMax`). Pass -1 for no limit. For XRP the issuer must be
* `xrpAccount()`; for non-XRP assets the issuer is `uSrcAccountID`
* or another account with a trust node.
* @param saDstAmountReq Exact amount to deliver to `uDstAccountID`
* (`sfAmount`). For XRP the issuer must be `xrpAccount()`; for
* non-XRP assets the issuer is `uDstAccountID` or another account
* with a trust node.
* @param uDstAccountID Account that must receive `saDstAmountReq`.
* @param uSrcAccountID Account supplying the input funds.
* @param spsPaths Candidate path hints from the transaction's `sfPaths`
* field. An empty set is valid when `pInputs->defaultPathsAllowed`
* is true.
* @param domainID Optional domain identifier for domain-scoped order
* books. When set, liquidity is restricted to the specified
* permissioned domain.
* @param registry Service registry supplying cross-cutting services,
* including the journal obtained via `registry.getJournal("Flow")`.
* @param pInputs Optional behavioral flags. Null is treated as the
* conservative default: no partial payment, default paths enabled,
* no quality limit.
* @return An `Output` holding `actualAmountIn`, `actualAmountOut`,
* `removableOffers`, and a `TER` result. On failure only
* `removableOffers` and `result()` are meaningful.
* @note `sendMax` is derived internally: it is set to `saMaxAmountReq`
* unless the sender and issuer are identical and the source/destination
* assets match, in which case `sendMax` is `nullopt` (no separate
* spending cap needed).
*/
static Output
rippleCalculate(
PaymentSandbox& view,
// Compute paths using this ledger entry set. Up to caller to actually
// apply to ledger.
// Issuer:
// XRP: xrpAccount()
// non-XRP: uSrcAccountID (for any issuer) or another account with
// trust node.
STAmount const& saMaxAmountReq, // --> -1 = no limit.
// Issuer:
// XRP: xrpAccount()
// non-XRP: uDstAccountID (for any issuer) or another account with
// trust node.
STAmount const& saMaxAmountReq,
STAmount const& saDstAmountReq,
AccountID const& uDstAccountID,
AccountID const& uSrcAccountID,
// A set of paths that are included in the transaction that we'll
// explore for liquidity.
STPathSet const& spsPaths,
std::optional<uint256> const& domainID,
ServiceRegistry& registry,
Input const* const pInputs = nullptr);
// The view we are currently working on
/** Mutable speculative ledger view used during path computation. */
PaymentSandbox& view;
// If the transaction fails to meet some constraint, still need to delete
// unfunded offers in a deterministic order (hence the ordered container).
//
// Offers that were found unfunded.
/** Offers that must be removed from the ledger regardless of payment outcome.
*
* Tracks structurally unfunded offers (as opposed to temporarily
* underfunded ones). Removal is performed in a deterministic order using
* this ordered container so that every validator produces identical ledger
* mutations and agrees on the resulting hash.
*/
boost::container::flat_set<uint256> permanentlyUnfundedOffers;
};

View File

@@ -0,0 +1,39 @@
/**
* @file AmountSpec.h
* @brief Include-compatibility shim for the legacy `AmountSpec` / `EitherAmount` types.
*
* This file is intentionally empty. It once defined two structs central to the
* multi-currency path-payment engine:
*
* - **`AmountSpec`** — a tagged union pairing an `XRPAmount` or `IOUAmount`
* with optional issuer and currency metadata, distinguishing the two cases
* via a `bool native` flag. It acted as a richly-annotated amount description
* that carried both the numeric value and the asset denomination in a single
* object.
*
* - **`EitherAmount`** — a lighter companion union used at `Step` interface
* boundaries where asset-type metadata was already implicit in the step's
* template parameters.
*
* Both structs were overhauled when MPT (Multi-Purpose Token) support was
* introduced (commit `dfcad6915`):
*
* - `EitherAmount` was extracted to `EitherAmount.h` and reimplemented as
* `std::variant<XRPAmount, IOUAmount, MPTAmount>`, constrained by the
* `StepAmount` concept from `protocol/Concepts.h`. The raw `union` + `bool`
* pattern was replaced with a type-safe, three-way discriminated union.
*
* - `AmountSpec` was retired. Its role is now fulfilled by the type system
* directly: `Step` subclasses are templated on `TIn`/`TOut` (both constrained
* to `StepAmount`), and the `Asset` / `Issue` / `MPTIssue` hierarchy carries
* issuer and denomination identity without a wrapper struct.
*
* The file is kept as an empty stub rather than removed because `StrandFlow.h`
* and `FlowDebugInfo.h` both `#include` it. Removing it would be a breaking
* change for any out-of-tree code that transitively relied on the inclusion.
*
* @note Any code that `#include`s this header directly should instead include:
* - `xrpl/tx/paths/detail/EitherAmount.h` for the `EitherAmount` type, or
* - `xrpl/protocol/XRPAmount.h`, `xrpl/protocol/IOUAmount.h`,
* `xrpl/protocol/MPTAmount.h` for the concrete amount types.
*/

View File

@@ -7,17 +7,55 @@
namespace xrpl {
/**
* Type-erased amount wrapper for the payment path engine.
*
* The `Step` abstraction uses virtual dispatch (`rev`/`fwd`) yet each
* concrete step works with a single, statically-typed amount —
* `XRPAmount`, `IOUAmount`, or `MPTAmount`. `EitherAmount` bridges this
* gap by holding `std::variant<XRPAmount, IOUAmount, MPTAmount>` behind a
* uniform value type that flows freely through the virtual step interface.
*
* Value semantics (no heap allocation, copyable, storable in `std::vector`)
* make this suitable for use in `FlowDebugInfo` diagnostic vectors and as
* return values from `Step::cachedIn()`/`cachedOut()`. A pointer-based
* design would require ownership management for what is fundamentally a
* lightweight numeric wrapper.
*
* All templated members are constrained by the `StepAmount` concept, which
* explicitly enumerates the three valid amount types. This closes the
* variant: you cannot accidentally construct from `STAmount` or any other
* compatible numeric type.
*
* @see Step::rev, Step::fwd, Step::cachedIn, Step::cachedOut
*/
struct EitherAmount
{
/** The underlying variant holding the active amount. */
std::variant<XRPAmount, IOUAmount, MPTAmount> amount;
/** Constructs a default (value-initialized) `EitherAmount`. */
explicit EitherAmount() = default;
/**
* Constructs an `EitherAmount` holding the given typed amount.
*
* @tparam T One of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
* @param a The amount value to store.
*/
template <StepAmount T>
explicit EitherAmount(T const& a) : amount(a)
{
}
/**
* Returns true if the variant currently holds type `T`.
*
* Call this before `get<T>()` when the active type is not statically
* guaranteed, to avoid the `std::logic_error` thrown on mismatch.
*
* @tparam T One of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
*/
template <StepAmount T>
[[nodiscard]] bool
holds() const
@@ -25,6 +63,18 @@ struct EitherAmount
return std::holds_alternative<T>(amount);
}
/**
* Returns a const reference to the held amount as type `T`.
*
* Throws `std::logic_error` if the variant does not hold `T`. This
* fail-fast design signals a programming error in the flow engine
* rather than a runtime data condition; mismatched access means the
* step dispatch wired the wrong amount type.
*
* @tparam T One of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
* @return Const reference to the held amount.
* @throws std::logic_error if `holds<T>()` is false.
*/
template <StepAmount T>
[[nodiscard]] T const&
get() const
@@ -35,6 +85,13 @@ struct EitherAmount
}
#ifndef NDEBUG
/**
* Writes a human-readable representation of the held amount to `stream`.
*
* Only compiled in debug builds. Uses `std::visit` with a template lambda
* to dispatch `to_string` to whichever concrete amount type is active,
* avoiding dead formatting overhead on the production hot path.
*/
friend std::ostream&
operator<<(std::ostream& stream, EitherAmount const& amt)
{
@@ -44,6 +101,18 @@ struct EitherAmount
#endif
};
/**
* Free-function accessor for `EitherAmount`; delegates to `amt.get<T>()`.
*
* Provides an alternative calling convention used throughout the flow engine
* (e.g., `get<XRPAmount>(either)` instead of `either.get<XRPAmount>()`).
* Throws `std::logic_error` via the member `get` if `T` is not the active type.
*
* @tparam T One of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
* @param amt The `EitherAmount` to extract from.
* @return Const reference to the held amount.
* @throws std::logic_error if `amt` does not hold type `T`.
*/
template <StepAmount T>
T const&
get(EitherAmount const& amt)

View File

@@ -1,14 +1,44 @@
/** @file
* In-place union helper for `boost::container::flat_set`.
*
* The payment-path engine accumulates sets of offer IDs that must be
* removed from the ledger (consumed, expired, or unfunded offers found
* during flow computation). Each strand and book step produces its own
* local removal set; `setUnion` merges those sets into a single
* accumulator so that cleanup can happen atomically after the flow
* completes, regardless of whether the overall payment succeeded.
*/
#pragma once
#include <boost/container/flat_set.hpp>
namespace xrpl {
/** Given two flat sets dst and src, compute dst = dst union src
@param dst set to store the resulting union, and also a source of elements
for the union
@param src second source of elements for the union
/** Merge @p src into @p dst in place, computing `dst = dst src`.
*
* Three optimizations make this efficient on the hot payment-flow path:
*
* 1. **Early exit** — returns immediately when @p src is empty, avoiding any
* allocation. This is the common case when a strand traverses a path
* that encounters no bad offers.
*
* 2. **Single pre-allocation** — `dst.reserve(dst.size() + src.size())`
* reserves enough capacity for the worst case (zero overlap) before any
* insertions, preventing repeated reallocation of the underlying
* contiguous array.
*
* 3. **Merge-style insert** — passing `ordered_unique_range_t{}` tells
* `flat_set` that @p src is already sorted and deduplicated (guaranteed
* because @p src is itself a `flat_set`). Boost performs an
* `inplace_merge`-style operation — O(n + m) — rather than inserting
* elements one at a time at O(n log n).
*
* In practice @p T is always `uint256` (offer ledger keys), but the
* function is generic over any element type that `flat_set` supports.
*
* @param dst Accumulator set; receives the union result in place.
* @param src Set of elements to fold into @p dst; left unchanged.
*/
template <class T>
void

View File

@@ -12,29 +12,111 @@
#include <sstream>
namespace xrpl::path::detail {
// Track performance information of a single payment
/**
* Per-payment telemetry accumulator for the flow engine.
*
* Constructed once per call to `flow()` and threaded down the call stack as
* a raw pointer. The `flow()` entry point accepts `FlowDebugInfo* = nullptr`,
* making the entire instrumentation path opt-in with zero overhead when the
* pointer is null. `RippleCalc::rippleCalculate` currently always passes
* `nullptr`, so no telemetry is gathered in production consensus paths — this
* struct is intended for testing, benchmarking, and developer tooling only.
*
* Two `boost::container::flat_map` members (reserved to 16 entries at
* construction) store named timing intervals (`timePoints`) and occurrence
* counters (`counts`). The flat-map layout is cache-friendly at the small
* sizes typical for a single payment execution.
*
* The single `PassInfo passInfo` member captures the sequence of incremental
* liquidity fills performed by the outer loop in `StrandFlow.h`.
*
* @note Callers must ensure the `FlowDebugInfo` object outlives any `Stopper`
* returned by `timeBlock()`, as `Stopper` holds a raw pointer back to
* this struct.
* @see FlowDebugInfo::timeBlock, FlowDebugInfo::PassInfo
*/
struct FlowDebugInfo
{
/** High-resolution clock used for all timing measurements. */
using clock = std::chrono::high_resolution_clock;
/** A point in time as recorded by `clock`. */
using time_point = clock::time_point;
/** Named timing intervals: tag → (start, end). Reserved to 16 entries. */
boost::container::flat_map<std::string, std::pair<time_point, time_point>> timePoints;
/** Named occurrence counters: tag → count. Reserved to 16 entries. */
boost::container::flat_map<std::string, std::size_t> counts;
/**
* Per-pass liquidity data for one complete `flow()` execution.
*
* The outer liquidity-selection loop in `StrandFlow.h` iterates, each
* iteration selecting the best-quality strand and routing an increment of
* the payment through it. `PassInfo` records one data point per iteration:
* - `in` / `out`: total amount consumed and delivered in that pass.
* - `numActive`: number of strands still active after that pass.
* - `liquiditySrcIn` / `liquiditySrcOut`: per-strand contributions within
* the pass, indexed as `[pass][strand]`.
*
* `nativeIn` and `nativeOut` are set at construction and drive amount
* serialization in `FlowDebugInfo::toString()` — XRP amounts are extracted
* via `get<XRPAmount>()`, IOU amounts via `get<IOUAmount>()`.
*/
struct PassInfo
{
PassInfo() = delete;
/**
* Constructs a `PassInfo` for a payment whose source and destination
* currency types are known up front.
*
* @param nativeIn True if the payment's input currency is XRP.
* @param nativeOut True if the payment's output currency is XRP.
*/
PassInfo(bool nativeIn, bool nativeOut) : nativeIn(nativeIn), nativeOut(nativeOut)
{
}
/** True if the payment's source currency is XRP. */
bool const nativeIn;
/** True if the payment's destination currency is XRP. */
bool const nativeOut;
/** Total amount consumed from senders, one entry per pass. */
std::vector<EitherAmount> in;
/** Total amount delivered to receivers, one entry per pass. */
std::vector<EitherAmount> out;
/** Number of active strands remaining after each pass. */
std::vector<size_t> numActive;
/**
* Per-strand input amounts, indexed as `[pass][strand]`.
*
* Each inner vector is opened by `newLiquidityPass()` and populated
* by `pushLiquiditySrc()`.
*/
std::vector<std::vector<EitherAmount>> liquiditySrcIn;
/**
* Per-strand output amounts, indexed as `[pass][strand]`.
*
* Each inner vector is opened by `newLiquidityPass()` and populated
* by `pushLiquiditySrc()`.
*/
std::vector<std::vector<EitherAmount>> liquiditySrcOut;
/**
* Reserves capacity in all parallel vectors to avoid repeated
* reallocations during the liquidity loop.
*
* @param s Expected number of passes.
*/
void
reserve(size_t s)
{
@@ -45,12 +127,26 @@ struct FlowDebugInfo
numActive.reserve(s);
}
/**
* Returns the number of passes recorded so far (equal to `in.size()`).
*/
[[nodiscard]] size_t
size() const
{
return in.size();
}
/**
* Records the aggregate result of one liquidity pass.
*
* Appends one entry to `in`, `out`, and `numActive`. Must be called
* once per outer-loop iteration after all per-strand contributions for
* that iteration have been pushed via `pushLiquiditySrc()`.
*
* @param inAmt Total amount consumed from senders in this pass.
* @param outAmt Total amount delivered to receivers in this pass.
* @param active Number of strands still active after this pass.
*/
void
pushBack(EitherAmount const& inAmt, EitherAmount const& outAmt, std::size_t active)
{
@@ -59,6 +155,16 @@ struct FlowDebugInfo
numActive.push_back(active);
}
/**
* Records the contribution of a single strand within the current pass.
*
* Appends to the back of `liquiditySrcIn` and `liquiditySrcOut`.
* `newLiquidityPass()` must be called before the first `pushLiquiditySrc()`
* call for each pass; an assertion fires if the inner vectors are empty.
*
* @param eIn Amount consumed from this strand's source in this pass.
* @param eOut Amount delivered by this strand in this pass.
*/
void
pushLiquiditySrc(EitherAmount const& eIn, EitherAmount const& eOut)
{
@@ -70,6 +176,14 @@ struct FlowDebugInfo
liquiditySrcOut.back().push_back(eOut);
}
/**
* Opens a new inner vector in `liquiditySrcIn` and `liquiditySrcOut`
* before the start of each liquidity pass.
*
* The capacity of the new inner vectors is hinted from the active-strand
* count of the previous pass (or 16 for the first pass), minimizing
* reallocations within the pass.
*/
void
newLiquidityPass()
{
@@ -82,9 +196,21 @@ struct FlowDebugInfo
}
};
/** Accumulated per-pass data for this payment execution. */
PassInfo passInfo;
FlowDebugInfo() = delete;
/**
* Constructs a `FlowDebugInfo` for a payment whose source and destination
* currency types are known up front.
*
* Pre-reserves the flat maps to 16 entries and `passInfo` to 64 passes to
* avoid repeated reallocations during typical payment executions.
*
* @param nativeIn True if the payment's input currency is XRP.
* @param nativeOut True if the payment's output currency is XRP.
*/
FlowDebugInfo(bool nativeIn, bool nativeOut) : passInfo(nativeIn, nativeOut)
{
timePoints.reserve(16);
@@ -92,6 +218,13 @@ struct FlowDebugInfo
passInfo.reserve(64);
}
/**
* Returns the wall-clock duration of the named timing block.
*
* @param tag The name used when the block was opened with `timeBlock()`.
* @return Elapsed time as a `std::chrono::duration<double>` (seconds).
* Returns zero and triggers `UNREACHABLE` if `tag` was never recorded.
*/
[[nodiscard]] auto
duration(std::string const& tag) const
{
@@ -109,6 +242,13 @@ struct FlowDebugInfo
return std::chrono::duration_cast<std::chrono::duration<double>>(t.second - t.first);
}
/**
* Returns the current occurrence count for a named counter.
*
* @param tag Counter name previously incremented via `inc()` or set via
* `setCount()`.
* @return The stored count, or 0 if the tag has never been recorded.
*/
[[nodiscard]] std::size_t
count(std::string const& tag) const
{
@@ -118,7 +258,28 @@ struct FlowDebugInfo
return i->second;
}
// Time the duration of the existence of the result
/**
* Times the duration of a lexical scope via RAII.
*
* Returns a `Stopper` whose constructor records `clock::now()` as both the
* start and the initial end of `tag`'s entry in `timePoints`. The
* destructor overwrites the end with a fresh `clock::now()`, capturing the
* elapsed time when the `Stopper` goes out of scope:
*
* ```cpp
* auto _ = flowDebugInfo->timeBlock("main");
* // ... timed work ...
* // duration captured automatically on scope exit
* ```
*
* The returned `Stopper` is move-constructible (required for RVO) but not
* copy-constructible, and it holds a raw pointer to this `FlowDebugInfo`.
* The parent must outlive the `Stopper`.
*
* @param name Unique tag for this timing interval.
* @return A `Stopper` RAII guard; keep it in scope for the duration to
* be measured.
*/
auto
timeBlock(std::string name)
{
@@ -141,6 +302,20 @@ struct FlowDebugInfo
return Stopper(std::move(name), *this);
}
/**
* Increments the occurrence count for a named counter.
*
* @param tag Counter name to increment.
*
* @note Contains a latent bug: when `tag` is absent from `counts`, the
* map inserts `counts[tag] = 1` and then attempts `++i->second` using
* the pre-insertion iterator `i`. For `flat_map`'s vector-backed
* storage, insertion can relocate elements, invalidating `i` and
* producing undefined behavior on the first use of any new tag. Because
* `FlowDebugInfo` is only used in diagnostic (non-production) code
* paths, this has not caused observable failures, but callers should
* prefer `setCount()` for new tags.
*/
void
inc(std::string const& tag)
{
@@ -152,36 +327,97 @@ struct FlowDebugInfo
++i->second;
}
/**
* Sets the occurrence count for a named counter to an explicit value.
*
* Overwrites any existing count for `tag`.
*
* @param tag Counter name.
* @param c Value to assign.
*/
void
setCount(std::string const& tag, std::size_t c)
{
counts[tag] = c;
}
/**
* Returns the total number of liquidity passes recorded.
*
* Equal to `passInfo.size()` (the number of entries in `passInfo.in`).
*/
[[nodiscard]] std::size_t
passCount() const
{
return passInfo.size();
}
/**
* Records the aggregate result of one liquidity pass.
*
* Delegates to `passInfo.pushBack()`. Called once per outer-loop iteration
* in `StrandFlow.h` after all per-strand contributions have been pushed.
*
* @param in Total amount consumed from senders in this pass.
* @param out Total amount delivered to receivers in this pass.
* @param activeStrands Number of strands still active after this pass.
*/
void
pushPass(EitherAmount const& in, EitherAmount const& out, std::size_t activeStrands)
{
passInfo.pushBack(in, out, activeStrands);
}
/**
* Records the contribution of a single strand within the current pass.
*
* Delegates to `passInfo.pushLiquiditySrc()`. Called once per strand
* selection inside the outer loop of `StrandFlow.h`.
*
* @param in Amount consumed from this strand's source.
* @param out Amount delivered by this strand.
*/
void
pushLiquiditySrc(EitherAmount const& in, EitherAmount const& out)
{
passInfo.pushLiquiditySrc(in, out);
}
/**
* Opens a new per-strand contribution slot for the upcoming pass.
*
* Must be called once at the start of each outer-loop iteration in
* `StrandFlow.h`, before any `pushLiquiditySrc()` calls for that pass.
* Delegates to `passInfo.newLiquidityPass()`.
*/
void
newLiquidityPass()
{
passInfo.newLiquidityPass();
}
/**
* Serializes the accumulated telemetry to a human-readable string.
*
* Always emits the total wall-clock duration of the `"main"` timed block
* and the number of passes. When `writePassInfo` is true, also emits the
* full sequence of per-pass in/out amounts, active strand counts, and
* per-strand liquidity arrays.
*
* Format (pass info omitted when `writePassInfo` is false):
* ```
* duration: <seconds>, pass_count: <N>[, in_pass: [...], out_pass: [...],
* num_active: [...][, l_src_in: [[...|...]], l_src_out: [[...|...]]]]
* ```
* Outer delimiter between passes is `;`; inner delimiter between strands
* within a pass is `|`.
*
* Amount serialization branches on `passInfo.nativeIn`/`nativeOut`: XRP
* amounts use `get<XRPAmount>()`, IOU amounts use `get<IOUAmount>()`.
*
* @param writePassInfo If true, include the full per-pass breakdown.
* @return Formatted diagnostic string suitable for logging.
*/
[[nodiscard]] std::string
toString(bool writePassInfo) const
{
@@ -298,6 +534,16 @@ struct FlowDebugInfo
}
};
/**
* Formats a single trust-line balance change entry into an output stream.
*
* Writes the element as `[sender|receiver|currency|amount]`, where the key
* is a `(AccountID, AccountID, Currency)` tuple and the value is the net
* `STAmount` change observed in a `PaymentSandbox`.
*
* @param ostr Destination stream.
* @param elem A map entry from `PaymentSandbox::balanceChanges()`.
*/
inline void
writeDiffElement(
std::ostringstream& ostr,
@@ -309,6 +555,19 @@ writeDiffElement(
ostr << '[' << get<0>(k) << '|' << get<1>(k) << '|' << get<2>(k) << '|' << v << ']';
};
/**
* Serializes a range of trust-line balance change entries to an output stream.
*
* Writes the full range as `[elem0;elem1;...]` using `writeDiffElement()` for
* each entry. Used by `balanceDiffsToString()` to produce a compact, machine-
* readable audit trail of every trust-line mutation caused by a payment.
*
* @tparam Iter Forward iterator over `(key, STAmount)` pairs as produced by
* `PaymentSandbox::balanceChanges()`.
* @param ostr Destination stream.
* @param begin Start of the range.
* @param end One past the end of the range.
*/
template <class Iter>
void
writeDiffs(std::ostringstream& ostr, Iter begin, Iter end)

View File

@@ -1,3 +1,14 @@
/** @file
* Freeze and NoRipple compliance guards for payment path steps.
*
* Every candidate step through the XRPL payment engine must pass both
* `checkFreeze` and `checkNoRipple` before the engine may use it. Both
* functions are read-only probes against the ledger state; neither modifies
* any entry. They are inlined here so each step-type translation unit
* embeds its own copy, avoiding call overhead on the hot path of payment
* execution.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -9,6 +20,41 @@
namespace xrpl {
/** Determine whether a trust-line hop is blocked by any freeze mechanism.
*
* Tests three freeze layers in order, returning `terNO_LINE` at the first
* violation:
*
* 1. **Global freeze** — if `dst` has `lsfGlobalFreeze` set, all IOUs it
* issues are inaccessible regardless of per-line flags.
* 2. **Directional per-line freeze** — the `dst`-side freeze flag
* (`lsfHighFreeze` or `lsfLowFreeze`, selected by canonical high/low
* ordering) blocks the step. This is asymmetric: an issuer may freeze a
* customer's line without affecting its own ability to redeem.
* 3. **Deep freeze** — either side's `lsfHighDeepFreeze`/`lsfLowDeepFreeze`
* bit blocks movement in both directions unconditionally.
*
* When `fixFrozenLPTokenTransfer` is active a fourth check applies: if
* `dst` is an AMM account (`sfAMMID` present), the corresponding AMM object
* is read and `isLPTokenFrozen()` tests whether the underlying pool assets
* are frozen. A missing AMM entry returns `tecINTERNAL` (indicates ledger
* corruption; marked `LCOV_EXCL_LINE`).
*
* @note In `DirectStep`, this check is intentionally skipped when the step
* is simultaneously first and last (`ctx.isFirst && ctx.isLast`),
* because a pure issue/redeem between the transaction's ultimate source
* and destination is inherently authorized and cannot be frozen.
*
* @param view The read-only ledger view.
* @param src The source account of this hop.
* @param dst The destination account of this hop (typically the IOU issuer
* whose freeze flags are authoritative for the line).
* @param currency The currency flowing across the trust line.
* @return `tesSUCCESS` if the hop is unblocked; `terNO_LINE` if any freeze
* layer blocks it; `tecINTERNAL` on AMM ledger corruption.
* @pre `src != dst` — self-loops must be excluded by the caller before
* invoking this function.
*/
inline TER
checkFreeze(
ReadView const& view,
@@ -60,6 +106,35 @@ checkFreeze(
return tesSUCCESS;
}
/** Determine whether the NoRipple setting on an intermediate account blocks
* a payment path through it.
*
* Evaluates the triple (`prev` → `cur` → `next`): transit through `cur` is
* rejected only when **both** of `cur`'s trust lines — the incoming line from
* `prev` and the outgoing line to `next` — carry `cur`'s NoRipple flag. The
* correct flag bit (`lsfHighNoRipple` or `lsfLowNoRipple`) is selected for
* each line using the canonical high/low account ordering.
*
* The AND semantics are deliberate: an intermediate account may have NoRipple
* enabled on some lines (to isolate certain counterparties) while leaving
* others open to routing. Transit is permitted as long as at least one side
* of the path through `cur` is "open."
*
* Returns `terNO_LINE` immediately if either trust line is absent — there is
* nothing to route through. A diagnostic message at `info` level is logged
* when `terNO_RIPPLE` is returned, naming all three accounts involved, to aid
* in tracing pathfinding failures.
*
* @param view The read-only ledger view.
* @param prev The account on the incoming side of `cur`.
* @param cur The intermediate account whose NoRipple constraints are checked.
* @param next The account on the outgoing side of `cur`.
* @param currency The currency flowing through both trust lines.
* @param j Journal used to log a diagnostic when the path is rejected.
* @return `tesSUCCESS` if transit through `cur` is permitted; `terNO_LINE`
* if either trust line does not exist; `terNO_RIPPLE` if both lines have
* NoRipple set from `cur`'s perspective.
*/
inline TER
checkNoRipple(
ReadView const& view,

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,19 @@
/**
* @file StrandFlow.h
* @brief Core payment flow execution engine: two-pass strand algorithm and
* multi-strand outer search loop.
*
* This header is the heart of the XRPL payment engine. It provides:
* - `flow<TInAmt, TOutAmt>(PaymentSandbox, Strand, ...)` — executes one
* strand via a reverse-then-forward two-pass algorithm.
* - `flow<TInAmt, TOutAmt>(PaymentSandbox, vector<Strand>, ...)` — the
* outer multi-strand search loop that consumes strands in quality order
* until the requested output is satisfied or all strands are dry.
*
* Every XRP payment — direct transfer, cross-currency, or offer cross — is
* ultimately executed through these functions. Callers are in `Flow.cpp`,
* which dispatches to the correct template instantiation via `std::visit`.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -23,26 +39,46 @@
namespace xrpl {
/** Result of flow() execution of a single Strand. */
/**
* Result of executing a single Strand through the two-pass flow algorithm.
*
* Bundles the actual amounts consumed/produced, the proposed ledger state
* (inside a `PaymentSandbox` that can be merged or discarded), the set of
* offers that must be removed regardless of payment outcome, and flags
* indicating whether the strand is now exhausted.
*
* @tparam TInAmt Input amount type (`XRPAmount`, `IOUAmount`, or `MPTAmount`).
* @tparam TOutAmt Output amount type.
*/
template <class TInAmt, class TOutAmt>
struct StrandResult
{
bool success = false; ///< Strand succeeded
TInAmt in = beast::kZERO; ///< Currency amount in
TOutAmt out = beast::kZERO; ///< Currency amount out
std::optional<PaymentSandbox> sandbox; ///< Resulting Sandbox state
boost::container::flat_set<uint256> ofrsToRm; ///< Offers to remove
// Num offers consumed or partially consumed (includes expired and unfunded
// offers)
bool success = false; ///< True if the strand produced non-zero output.
TInAmt in = beast::kZERO; ///< Actual input consumed by the strand.
TOutAmt out = beast::kZERO; ///< Actual output produced by the strand.
std::optional<PaymentSandbox> sandbox; ///< Proposed ledger mutations; empty on failure.
boost::container::flat_set<uint256> ofrsToRm; ///< Offers to remove from the ledger (bad/expired), even on failure.
/// Number of offers consumed or partially consumed (includes expired and
/// unfunded offers). Counts toward `maxOffersToConsider` in the outer loop.
std::uint32_t ofrsUsed = 0;
// strand can be inactive if there is no more liquidity or too many offers
// have been consumed
bool inactive = false; ///< Strand should not considered as a further
///< source of liquidity (dry)
/// True when the strand has no remaining liquidity or has consumed too many
/// offers. An inactive strand is not returned to the `next_` candidate set
/// for future rounds.
bool inactive = false;
/** Strand result constructor */
/** Constructs a default (failed/dry) result with no sandbox. */
StrandResult() = default;
/**
* Constructs a successful strand result.
*
* @param strand The strand that was executed (used to count offers).
* @param in Actual input consumed.
* @param out Actual output produced.
* @param sandbox Sandbox containing the resulting ledger mutations.
* @param ofrsToRemoveMember Offers to remove regardless of payment success.
* @param inactive True if the strand is now exhausted.
*/
StrandResult(
Strand const& strand,
TInAmt const& in,
@@ -60,6 +96,14 @@ struct StrandResult
{
}
/**
* Constructs a failed (dry) strand result that still carries offers to
* remove. Used when the strand produced no usable liquidity but did
* encounter unfunded or expired offers that must be cleaned up.
*
* @param strand The strand that was executed.
* @param ofrsToRemoveMember Offers to remove even though the strand failed.
*/
StrandResult(Strand const& strand, boost::container::flat_set<uint256> ofrsToRemoveMember)
: ofrsToRm(std::move(ofrsToRemoveMember)), ofrsUsed(offersUsed(strand))
{
@@ -67,15 +111,40 @@ struct StrandResult
};
/**
Request `out` amount from a strand
@param baseView Trust lines and balances
@param strand Steps of Accounts to ripple through and offer books to use
@param maxIn Max amount of input allowed
@param out Amount of output requested from the strand
@param j Journal to write log messages to
@return Actual amount in and out from the strand, errors, offers to remove,
and payment sandbox
* Execute a single payment strand and request `out` units of output.
*
* Implements the classical reverse-then-forward two-pass algorithm:
*
* **Reverse pass (right-to-left):** Each step is called via `rev()` in
* reverse order, threading the requested output backwards to determine how
* much input each step needs. When a step cannot fully satisfy its request
* (the *limiting step*), the sandbox is discarded and that step is
* re-executed with the capped amount. If the limiting step is index 0 and
* would exceed `maxIn`, the step is re-executed forward with exactly `maxIn`.
*
* **Forward pass (left-to-right from limiting step):** After the reverse
* pass, every step to the right of the limiting step is called via `fwd()`
* to propagate the actual amounts and complete the sandbox state.
*
* A debug-only re-validation (`#ifndef NDEBUG`) re-executes the whole strand
* forward via `validFwd()` to catch inconsistencies in step implementations.
*
* Two early exits apply: an empty strand returns immediately, and a direct
* XRP-to-XRP strand (no exchange needed) returns a dry result without any
* execution. Any `FlowException` is caught and converted to a dry result so
* a bad offer in one strand does not abort the entire payment.
*
* @tparam TInAmt Input amount type (`XRPAmount`, `IOUAmount`, `MPTAmount`).
* @tparam TOutAmt Output amount type.
* @param baseView Read-only view of trust lines and balances before this
* strand executes (the "all funds" baseline).
* @param strand Ordered list of `Step` objects describing the route.
* @param maxIn If present, caps the total input this strand may consume.
* @param out Amount of output requested from the strand.
* @param j Journal for diagnostic log messages.
* @return A `StrandResult` containing the actual amounts, the proposed
* ledger mutations in a `PaymentSandbox`, offers to remove, and the
* `inactive` flag indicating whether the strand is now exhausted.
*/
template <class TInAmt, class TOutAmt>
StrandResult<TInAmt, TOutAmt>
@@ -106,9 +175,9 @@ flow(
std::size_t limitingStep = strand.size();
std::optional<PaymentSandbox> sb(&baseView);
// The "all funds" view determines if an offer becomes unfunded or is
// found unfunded
// These are the account balances before the strand executes
// afView is the "all funds" snapshot: account balances as they were
// before any step in this strand executed. Steps use it to determine
// whether an offer is currently funded (vs. the evolving `sb` view).
std::optional<PaymentSandbox> afView(&baseView);
EitherAmount limitStepOut;
{
@@ -124,12 +193,12 @@ flow(
if (i == 0 && maxIn && *maxIn < get<TInAmt>(r.first))
{
// limiting - exceeded maxIn
// Throw out previous results
// Step 0 would exceed maxIn: re-execute forward with
// exactly maxIn rather than in reverse, because step 0
// has no predecessor to supply a revised input.
sb.emplace(&baseView);
limitingStep = i;
// re-execute the limiting step
r = strand[i]->fwd(*sb, *afView, ofrsToRm, EitherAmount(*maxIn));
limitStepOut = r.second;
@@ -140,9 +209,9 @@ flow(
}
if (get<TInAmt>(r.first) != *maxIn)
{
// Something is very wrong
// throwing out the sandbox can only increase liquidity
// yet the limiting is still limiting
// Invariant violation: discarding the old sandbox can
// only increase available liquidity, so re-executing
// with maxIn should always consume exactly maxIn.
// LCOV_EXCL_START
JLOG(j.fatal())
<< "Re-executed limiting step failed. r.first: "
@@ -156,13 +225,14 @@ flow(
}
else if (!strand[i]->equalOut(r.second, stepOut))
{
// limiting
// Throw out previous results
// This step is the limiting step: it produced less than
// requested. Discard the partial sandbox (fresh view gives
// the step full liquidity again) and re-execute in reverse
// with the capped amount to record the definitive state.
sb.emplace(&baseView);
afView.emplace(&baseView);
limitingStep = i;
// re-execute the limiting step
stepOut = r.second;
r = strand[i]->rev(*sb, *afView, ofrsToRm, stepOut);
limitStepOut = r.second;
@@ -176,9 +246,9 @@ flow(
}
if (!strand[i]->equalOut(r.second, stepOut))
{
// Something is very wrong
// throwing out the sandbox can only increase liquidity
// yet the limiting is still limiting
// Invariant violation: a fresh sandbox can only
// increase liquidity, so the re-executed limiting step
// must be able to deliver at least as much as before.
// LCOV_EXCL_START
#ifndef NDEBUG
JLOG(j.fatal())
@@ -195,8 +265,7 @@ flow(
}
}
// prev node needs to produce what this node wants to consume
stepOut = r.first;
stepOut = r.first; // propagate: predecessor must deliver what this step will consume
}
}
@@ -214,9 +283,9 @@ flow(
}
if (!strand[i]->equalIn(r.first, stepIn))
{
// The limits should already have been found, so executing a
// strand forward from the limiting step should not find a
// new limit
// Invariant violation: the reverse pass already found the
// global limiting step, so the forward pass must never
// encounter a new limit.
// LCOV_EXCL_START
#ifndef NDEBUG
JLOG(j.fatal()) << "Re-executed forward pass failed. r.first: " << r.first
@@ -281,17 +350,37 @@ flow(
}
/// @cond INTERNAL
/**
* Aggregate result of the multi-strand payment flow loop.
*
* Accumulates the total input consumed and output produced across all
* successful strand rounds, the merged `PaymentSandbox` representing all
* proposed ledger changes, offers that must be cleaned up regardless of
* payment success, and a `TER` result code.
*
* @tparam TInAmt Input amount type.
* @tparam TOutAmt Output amount type.
*/
template <class TInAmt, class TOutAmt>
struct FlowResult
{
TInAmt in = beast::kZERO;
TOutAmt out = beast::kZERO;
std::optional<PaymentSandbox> sandbox;
boost::container::flat_set<uint256> removableOffers;
TER ter = temUNKNOWN;
TInAmt in = beast::kZERO; ///< Total input consumed across all rounds.
TOutAmt out = beast::kZERO; ///< Total output delivered across all rounds.
std::optional<PaymentSandbox> sandbox; ///< Merged sandbox; present only on success.
boost::container::flat_set<uint256> removableOffers; ///< Offers to remove whether or not payment succeeds.
TER ter = temUNKNOWN; ///< `tesSUCCESS` or a TEC/TEF error code.
/** Constructs a default (unknown-error) result. */
FlowResult() = default;
/**
* Constructs a successful result with all amounts and ledger state.
*
* @param in Total input consumed.
* @param out Total output delivered.
* @param sandbox Merged sandbox holding all ledger mutations.
* @param ofrsToRm Offers to remove from the ledger.
*/
FlowResult(
TInAmt const& in,
TOutAmt const& out,
@@ -305,11 +394,27 @@ struct FlowResult
{
}
/**
* Constructs a failure result carrying only the offers to remove.
* Used when the flow loop exits with no usable liquidity at all.
*
* @param ter Error code describing the failure.
* @param ofrsToRm Offers to remove even though the payment failed.
*/
FlowResult(TER ter, boost::container::flat_set<uint256> ofrsToRm)
: removableOffers(std::move(ofrsToRm)), ter(ter)
{
}
/**
* Constructs a partial-failure result that includes the amounts achieved
* before the flow loop stopped (e.g., `tecPATH_PARTIAL`).
*
* @param ter Error code (typically `tecPATH_PARTIAL`).
* @param in Input consumed up to the point of failure.
* @param out Output delivered up to the point of failure.
* @param ofrsToRm Offers to remove regardless of payment outcome.
*/
FlowResult(
TER ter,
TInAmt const& in,
@@ -322,6 +427,22 @@ struct FlowResult
/// @endcond
/// @cond INTERNAL
/**
* Compute an upper bound on the exchange rate (quality) achievable by a strand.
*
* Calls `qualityUpperBound()` on each step in sequence, composing the
* per-step bounds via `composedQuality()` while propagating the `DebtDirection`
* (distinguishing redemption from issuance, which affects transfer fees).
* Returns `std::nullopt` if any step is provably dry.
*
* This estimate may be optimistic — unfunded offers at the tip of a book can
* make the actual quality lower — but it is a sound heuristic for ranking
* candidate strands in `ActiveStrands::activateNext()`.
*
* @param v Read-only ledger view.
* @param strand The strand to evaluate.
* @return An upper-bound `Quality`, or `std::nullopt` if the strand is dry.
*/
inline std::optional<Quality>
qualityUpperBound(ReadView const& v, Strand const& strand)
{
@@ -344,13 +465,29 @@ qualityUpperBound(ReadView const& v, Strand const& strand)
/// @endcond
/// @cond INTERNAL
/** Limit remaining out only if one strand and limitQuality is included.
* Targets one path payment with AMM where the average quality is linear
* and instant quality is quadratic function of output. Calculating quality
* function for the whole strand enables figuring out required output
* to produce requested strand's limitQuality. Reducing the output,
* increases quality of AMM steps, increasing the strand's composite
* quality as the result.
/**
* Reduce the requested output to the amount that exactly satisfies
* `limitQuality` when there is exactly one active strand.
*
* This optimization applies only to AMM-backed strands where quality is not
* constant: for an AMM, the average exchange rate is a quadratic function of
* output, so requesting less output yields a better average quality. The
* function collects per-step `QualityFunction` objects and combines them into
* a strand-level quality function, then solves for the output that achieves
* `limitQuality` via `QualityFunction::outFromAvgQ()`.
*
* A relative-distance guard (`withinRelativeDistance(..., 1e-9)`) absorbs
* floating-point rounding and avoids spurious adjustments. If the quality
* function is constant (no AMM steps), or if any step does not provide a
* quality function, the function returns `remainingOut` unchanged.
*
* @tparam TOutAmt Output amount type.
* @param v Ledger view used by step quality-function queries.
* @param strand The single active strand.
* @param remainingOut Current remaining output request; returned unchanged
* if no adjustment is possible.
* @param limitQuality Minimum acceptable average quality.
* @return The adjusted output cap, which is ≤ `remainingOut`.
*/
template <typename TOutAmt>
inline TOutAmt
@@ -382,7 +519,8 @@ limitOut(
}
}
// QualityFunction is constant
// If every step is a fixed-rate hop the quality function is constant;
// no adjustment is possible or necessary.
if (!qf || qf->isConst())
return remainingOut;
@@ -407,7 +545,9 @@ limitOut(
return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()};
}
}();
// A tiny difference could be due to the round off
// A computed `out` that is negligibly close to `remainingOut` (within 1e-9
// relative) is treated as equal to avoid spurious reductions caused by
// floating-point rounding in `outFromAvgQ`.
if (withinRelativeDistance(out, remainingOut, Number(1, -9)))
return remainingOut;
return std::min(out, remainingOut);
@@ -415,22 +555,40 @@ limitOut(
/// @endcond
/// @cond INTERNAL
/* Track the non-dry strands
flow will search the non-dry strands (stored in `cur_`) for the best
available liquidity If flow doesn't use all the liquidity of a strand, that
strand is added to `next_`. The strands in `next_` are searched after the
current best liquidity is used.
/**
* Lazy candidate set for the multi-strand liquidity search loop.
*
* Tracks which strands are still eligible to provide liquidity across
* outer iterations. Maintains two sets:
* - `cur_` — strands being evaluated in the *current* round.
* - `next_` — strands to evaluate in the *next* round (strands that still
* had remaining liquidity after the current round, plus any
* strand not yet reached in `cur_`).
*
* The probe-and-push pattern guarantees that only one strand is consumed per
* outer iteration: the loop picks the first strand from `cur_` that yields
* usable liquidity (`best`), pushes all remaining unchecked `cur_` strands to
* `next_`, and returns `best` to `next_` if the strand is not exhausted.
* This ensures high-quality strands that only partially satisfy the remaining
* output remain in contention for subsequent rounds.
*
* @note `activateNext()` uses `std::ranges::stable_sort` intentionally: the
* stability is required for deterministic tie-breaking across different
* C++ standard library implementations, which is critical for consensus.
*/
class ActiveStrands
{
private:
// Strands to be explored for liquidity
std::vector<Strand const*> cur_;
// Strands that may be explored for liquidity on the next iteration
std::vector<Strand const*> next_;
std::vector<Strand const*> cur_; ///< Strands under evaluation this round.
std::vector<Strand const*> next_; ///< Candidates for the next round.
public:
/**
* Initialise from all candidate strands; all are placed in `next_` so
* that the first call to `activateNext()` populates `cur_`.
*
* @param strands The full set of payment path strands.
*/
ActiveStrands(std::vector<Strand> const& strands)
{
cur_.reserve(strands.size());
@@ -439,13 +597,21 @@ public:
next_.push_back(&strand);
}
// Start a new iteration in the search for liquidity
// Set the current strands to the strands in `next_`
/**
* Advance to the next round: sort `next_` by quality upper-bound
* (best first) and swap it into `cur_`.
*
* Strands whose `qualityUpperBound` falls below `limitQuality` are pruned
* and will never appear in `cur_` again. The sort is a `stable_sort` to
* guarantee deterministic ordering when two strands have equal quality
* upper bounds — required for consensus across nodes.
*
* @param v Read-only ledger view for quality estimation.
* @param limitQuality If present, strands below this threshold are pruned.
*/
void
activateNext(ReadView const& v, std::optional<Quality> const& limitQuality)
{
// add the strands in `next_` to `cur_`, sorted by theoretical quality.
// Best quality first.
cur_.clear();
if (!next_.empty())
{
@@ -464,19 +630,17 @@ public:
{
if (limitQuality && *qual < *limitQuality)
{
// If a strand's quality is ever over limitQuality
// it is no longer part of the candidate set. Note
// that when transfer fees are charged, and an
// account goes from redeeming to issuing then
// strand quality _can_ increase; However, this is
// an unusual corner case.
// Prune this strand permanently. Transfer fees can
// cause quality to increase as an account moves from
// redeeming to issuing, but this is rare enough that
// we accept the occasional premature prune.
continue;
}
strandQualities.emplace_back(*qual, strand);
}
}
// must stable sort for deterministic order across different c++
// standard library implementations
// stable_sort is mandatory: deterministic tie-breaking is a
// consensus requirement across different stdlib implementations.
std::ranges::stable_sort(
strandQualities,
@@ -495,6 +659,13 @@ public:
std::swap(cur_, next_);
}
/**
* Return the strand at position `i` in the current round's candidate set.
*
* @param i Zero-based index into `cur_`.
* @return Pointer to the strand, or `nullptr` if `i` is out of range
* (which should never happen in correct usage).
*/
[[nodiscard]] Strand const*
get(size_t i) const
{
@@ -508,13 +679,28 @@ public:
return cur_[i];
}
/**
* Return a strand to the `next_` candidate set for consideration in the
* following round. Called for the winning `best` strand when it still
* has remaining liquidity (i.e., `!f.inactive`).
*
* @param s Strand to preserve for the next round.
*/
void
push(Strand const* s)
{
next_.push_back(s);
}
// Push the strands from index i to the end of cur_ to next_
/**
* Move all strands from `cur_[i..end)` to `next_`.
*
* Called after the probe-and-push loop selects a winning strand: all
* strands after the winner in `cur_` (which were not yet evaluated this
* round) are deferred to `next_` so they remain in contention.
*
* @param i Index of the first strand to defer (inclusive).
*/
void
pushRemainingCurToNext(size_t i)
{
@@ -523,6 +709,7 @@ public:
next_.insert(next_.end(), std::next(cur_.begin(), i), cur_.end());
}
/** Number of strands in the current round's candidate set. */
[[nodiscard]] auto
size() const
{
@@ -532,25 +719,58 @@ public:
/// @endcond
/**
Request `out` amount from a collection of strands
Attempt to fulfill the payment by using liquidity from the strands in order
from least expensive to most expensive
@param baseView Trust lines and balances
@param strands Each strand contains the steps of accounts to ripple through
and offer books to use
@param outReq Amount of output requested from the strand
@param partialPayment If true allow less than the full payment
@param offerCrossing If true offer crossing, not handling a standard payment
@param limitQuality If present, the minimum quality for any strand taken
@param sendMaxST If present, the maximum STAmount to send
@param j Journal to write journal messages to
@param ammContext counts iterations with AMM offers
@param flowDebugInfo If pointer is non-null, write flow debug info here
@return Actual amount in and out from the strands, errors, and payment
sandbox
*/
* Execute a multi-strand payment and request `outReq` units of output.
*
* This is the top-level payment loop. It iterates over candidate strands in
* quality order (best first), consuming one strand per outer iteration until
* `outReq` is satisfied, all strands are exhausted, or a safety limit is hit.
*
* **Safety limits:**
* - `maxTries = 1000` — maximum outer iterations; returns
* `telFAILED_PROCESSING` if exceeded.
* - `maxOffersToConsider = 1500` — maximum cumulative offers consumed across
* all strands; triggers early exit with the best result so far.
*
* **Precision:** Rather than accumulating a running total (lossy for IOU
* arithmetic), each round's amounts are collected into `flat_multiset`
* containers (`savedIns`, `savedOuts`) and summed smallest-to-largest via
* `std::accumulate` to minimise floating-point drift.
*
* **Offer cleanup:** Bad offers (`ofrsToRm`) are deleted from the sandbox
* immediately after each strand attempt — even failed strands — so they
* cannot interfere with subsequent strands. A superset (`ofrsToRmOnFail`)
* is returned to callers for cleanup if the payment ultimately fails.
*
* **FillOrKill semantics (offer crossing):** The final section branches on the
* `fixFillOrKill` amendment and the `offerCrossing` mode:
* 1. Without `tfSell` (or pre-amendment): if `actualOut < outReq`, kill.
* 2. With `tfSell` (or pre-amendment): if `remainingIn != 0`, kill (all of
* `TakerGets` must be spent).
*
* **AMM integration:** `ammContext.setMultiPath(n > 1)` informs the AMM
* whether it is competing with other active strands (affects virtual offer
* sizing). `ammContext.clear()` resets the per-strand used flag before each
* strand attempt. `ammContext.update()` increments the AMM iteration counter
* after each successful round.
*
* @tparam TInAmt Input amount type (`XRPAmount`, `IOUAmount`, `MPTAmount`).
* @tparam TOutAmt Output amount type.
* @param baseView Read-only view of trust lines and balances.
* @param strands All candidate payment path strands.
* @param outReq Requested output amount.
* @param partialPayment If true, a partial delivery is acceptable.
* @param offerCrossing Indicates whether this is offer crossing and which
* mode (`No`, `Sell`, or full `FillOrKill`).
* @param limitQuality If present, only strands meeting this quality
* threshold are used.
* @param sendMaxST If present, caps the total input consumed.
* @param j Journal for diagnostic log messages.
* @param ammContext Tracks AMM iteration state across rounds.
* @param flowDebugInfo If non-null, receives per-round debug telemetry.
* @return A `FlowResult` with the aggregate amounts, merged sandbox, offers to
* clean up, and a `TER` result code (`tesSUCCESS`,
* `tecPATH_PARTIAL`, `tecPATH_DRY`, or `telFAILED_PROCESSING`).
*/
template <StepAmount TInAmt, StepAmount TOutAmt>
FlowResult<TInAmt, TOutAmt>
flow(
@@ -565,8 +785,7 @@ flow(
AMMContext& ammContext,
path::detail::FlowDebugInfo* flowDebugInfo = nullptr)
{
// Used to track the strand that offers the best quality (output/input
// ratio)
/** Holds the winning strand's result for the current outer iteration. */
struct BestStrand
{
TInAmt in;
@@ -591,26 +810,22 @@ flow(
std::uint32_t const maxOffersToConsider = 1500;
std::uint32_t offersConsidered = 0;
// There is a bug in gcc that incorrectly warns about using uninitialized
// values if `remainingIn` is initialized through a copy constructor. We can
// get similar warnings for `sendMax` if it is initialized in the most
// natural way. Using `make_optional`, allows us to work around this bug.
// GCC incorrectly warns about uninitialized values when initialising
// std::optional<TInAmt> via a copy constructor. Using make_optional
// avoids this false positive without semantic change.
TInAmt const sendMaxInit = sendMaxST ? toAmount<TInAmt>(*sendMaxST) : TInAmt{beast::kZERO};
std::optional<TInAmt> const sendMax =
(sendMaxST && sendMaxInit >= beast::kZERO) ? std::make_optional(sendMaxInit) : std::nullopt;
std::optional<TInAmt> remainingIn = !!sendMax ? std::make_optional(sendMaxInit) : std::nullopt;
// std::optional<TInAmt> remainingIn{sendMax};
TOutAmt remainingOut(outReq);
PaymentSandbox sb(&baseView);
// non-dry strands
ActiveStrands activeStrands(strands);
// Keeping a running sum of the amount in the order they are processed
// will not give the best precision. Keep a collection so they may be summed
// from smallest to largest
// Amounts are accumulated in sorted flat_multiset containers and summed
// smallest-to-largest to minimise floating-point precision loss.
boost::container::flat_multiset<TInAmt> savedIns;
savedIns.reserve(maxTries);
boost::container::flat_multiset<TOutAmt> savedOuts;
@@ -623,8 +838,8 @@ flow(
return std::accumulate(col.begin() + 1, col.end(), *col.begin());
};
// These offers only need to be removed if the payment is not
// successful
// Accumulates all bad offers seen during the flow loop. Returned to the
// caller for removal even if the payment ultimately fails.
boost::container::flat_set<uint256> ofrsToRmOnFail;
while (remainingOut > beast::kZERO && (!remainingIn || *remainingIn > beast::kZERO))
@@ -639,7 +854,8 @@ flow(
ammContext.setMultiPath(activeStrands.size() > 1);
// Limit only if one strand and limitQuality
// Apply AMM quality-function optimisation only when there is a single
// active strand and a limitQuality threshold is set.
auto const limitRemainingOut = [&]() {
if (activeStrands.size() == 1 && limitQuality)
{
@@ -662,9 +878,9 @@ flow(
// should not happen
continue;
}
// Clear AMM liquidity used flag. The flag might still be set if
// the previous strand execution failed. It has to be reset
// since this strand might not have AMM liquidity.
// Reset AMM used-flag before each strand: a failed prior strand
// may have left the flag set, which would incorrectly mark the
// next (unrelated) strand as having consumed AMM liquidity.
ammContext.clear();
if (offerCrossing != OfferCrossing::No && limitQuality)
{
@@ -674,8 +890,7 @@ flow(
}
auto f = flow<TInAmt, TOutAmt>(sb, *strand, remainingIn, limitRemainingOut, j);
// rm bad offers even if the strand fails
setUnion(ofrsToRm, f.ofrsToRm);
setUnion(ofrsToRm, f.ofrsToRm); // collect bad offers even on strand failure
offersConsidered += f.ofrsUsed;
@@ -694,9 +909,8 @@ flow(
JLOG(j.trace()) << "New flow iter (iter, in, out): " << curTry - 1 << " "
<< to_string(f.in) << " " << to_string(f.out);
// limitOut() finds output to generate exact requested
// limitQuality. But the actual limit quality might be slightly
// off due to the round off.
// `limitOut()` targets exact limitQuality but actual quality may
// differ by ~1e-7 due to rounding; accept values within that band.
if (limitQuality && q < *limitQuality &&
(!adjustedRemOut || !withinRelativeDistance(q, *limitQuality, Number(1, -7))))
{

View File

@@ -4,6 +4,27 @@
namespace xrpl {
/** Executes the AccountDelete transaction on the XRP Ledger.
*
* AccountDelete is the most structurally complex transactor in the system:
* it tears down an entire account by erasing every owned ledger object,
* transferring the remaining XRP balance to a destination, and finally
* removing the account root SLE.
*
* `kCONSEQUENCES_FACTORY = Blocker` prevents the transaction queue from
* scheduling any transaction from the same account behind a pending delete,
* because deletion clears sequence numbers and owned objects, making any
* queued follower semantically undefined.
*
* @note An account can only be deleted if it satisfies all of:
* its sequence number is at least 256 below the current ledger index,
* `FirstNFTokenSequence + MintedNFTokens + 255` does not exceed the
* current ledger index, it has no live NFTs and no outstanding obligations
* (trust lines, escrows, etc.), and the owner directory contains at most
* `maxDeletableDirEntries` entries.
*
* @see Transactor
*/
class AccountDelete : public Transactor
{
public:
@@ -13,27 +34,132 @@ public:
{
}
/** Gate the transaction on the `featureCredentials` amendment.
*
* If `sfCredentialIDs` is present in the transaction, this method
* returns `false` unless the `featureCredentials` amendment is enabled,
* causing `invokePreflight` to return `temDISABLED`. Called by the
* preflight framework before any field-level validation.
*
* @param ctx The preflight context carrying the transaction and rules.
* @return `true` if the transaction may proceed to `preflight`; `false`
* if the required amendment is not yet active.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Perform stateless early rejection of the transaction.
*
* Rejects self-sends (`sfAccount == sfDestination` → `temDST_IS_SRC`)
* and validates the format of any `sfCredentialIDs` field via
* `credentials::checkFields`. No ledger state is consulted.
*
* @param ctx The preflight context carrying the transaction and rules.
* @return `tesSUCCESS` if the transaction passes static checks, or a
* `tem*` code describing the first validation failure.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Compute the fee as one owner-reserve unit.
*
* Overrides the standard reference-fee calculation to call
* `calculateOwnerReserveFee()`, pricing deletion at one reserve
* increment. This makes the operation economically meaningful and
* discourages spam.
*
* @param view Read-only view used to look up current reserve settings.
* @param tx The AccountDelete transaction.
* @return The required fee in drops (one owner-reserve unit).
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Validate ledger state against all deletion preconditions.
*
* Performs every stateful check required before account deletion may
* proceed. Checks are applied in this order:
*
* 1. Destination account must exist (`tecNO_DST`).
* 2. Destination tag must be supplied if required by the destination
* (`tecDST_TAG_NEEDED`).
* 3. If `sfCredentialIDs` are absent, the destination's `lsfDepositAuth`
* flag must be satisfied by a pre-authorized entry; if credentials are
* present this check is deferred to `doApply` so that expiry is caught
* at apply-time rather than claim-time.
* 4. The account must have no live NFTs (minted ≠ burned → `tecHAS_OBLIGATIONS`).
* 5. The account's sequence must be ≥ 256 below the current ledger index
* to prevent transaction replay after account resurrection (`tecTOO_SOON`).
* 6. `FirstNFTokenSequence + MintedNFTokens + 255` must not exceed the
* current ledger index, preventing duplicate NFTokenIDs via authorized
* minters after resurrection (`tecTOO_SOON`).
* 7. Every entry in the owner directory must have a registered deleter
* (i.e. be a non-obligation type such as an offer, ticket, or signer
* list); any unrecognized type returns `tecHAS_OBLIGATIONS`.
* 8. The owner directory must not exceed `maxDeletableDirEntries`
* entries (`tefTOO_BIG`).
*
* @param ctx The preclaim context carrying a read-only ledger view.
* @return `tesSUCCESS` if all conditions are met, or the appropriate
* `tec`/`tef` code describing the first unmet condition.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the account teardown and XRP transfer.
*
* Assumes `preclaim` has already validated ledger state and proceeds to:
*
* 1. If `sfCredentialIDs` are present, call `verifyDepositPreauth` to
* confirm authorization and detect expired credentials (deferred from
* `preclaim`).
* 2. Walk the owner directory via `cleanupOnAccountDelete`, invoking the
* appropriate deleter for each entry.
* 3. Transfer the remaining XRP balance to the destination and call
* `ctx_.deliver()` to record the delivered amount.
* 4. Erase the (now empty) owner directory root node; a non-empty
* directory at this point is treated as a ledger integrity error.
* 5. Clear `lsfPasswordSpent` on the destination if XRP was received
* and the flag was set.
* 6. Erase the source account root SLE.
*
* @return `tesSUCCESS` on successful deletion, or a `tec`/`tef` code
* if an unexpected error is encountered during cleanup.
*/
TER
doApply() override;
/** Accumulate per-entry state for transaction-specific invariant checks.
*
* Currently a no-op placeholder; AccountDelete relies on the protocol-
* level invariants (`AccountRootsNotDeleted`, `AccountRootsDeletedClean`,
* `XRPNotCreated`, etc.) rather than defining additional per-entry checks.
* Reserved for future transaction-specific invariants.
*
* @param isDelete `true` if the entry was erased.
* @param before SLE state before the transaction (nullptr if new).
* @param after SLE state after the transaction (nullptr for deletions
* is not guaranteed; use `isDelete` to detect deletions).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Verify transaction-specific post-conditions after all entries are visited.
*
* Currently a no-op placeholder that always returns `true`.
* AccountDelete's correctness is enforced by the protocol-level invariant
* checkers. Reserved for future transaction-specific invariants.
*
* @param tx The transaction being applied.
* @param result The tentative TER result so far.
* @param fee The fee consumed by the transaction.
* @param view Read-only view of the ledger after the transaction.
* @param j Journal for logging invariant failures.
* @return Always `true`; no transaction-specific invariants are defined yet.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -5,6 +5,25 @@
namespace xrpl {
/** Processes AccountSet transactions, the primary mechanism for configuring an
* account root entry on the XRP Ledger.
*
* Covers behavioral flags (RequireAuth, DisableMaster, NoFreeze, GlobalFreeze,
* DepositAuth, disallow-incoming variants), metadata fields (Domain, EmailHash,
* MessageKey, WalletLocator), economic parameters (TransferRate, TickSize), and
* NFT settings (authorized minter).
*
* The `ConsequencesFactory` is `Custom` because most AccountSet transactions are
* normal but those that touch `asfRequireAuth`, `asfDisableMaster`,
* `asfAccountTxnID`, or the legacy `tfRequireAuth`/`tfOptionalAuth` flags are
* classified as blockers, which prevent subsequent same-account transactions from
* being queued until the blocker confirms.
*
* @note Flag changes accept two parallel encodings for historical compatibility:
* the legacy transaction-flags bitfield (`tfRequireAuth`, etc.) and the
* modern `sfSetFlag`/`sfClearFlag` fields (`asfRequireAuth`, etc.). Both
* paths are validated and applied in all three pipeline phases.
*/
class AccountSet : public Transactor
{
public:
@@ -14,30 +33,160 @@ public:
{
}
/** Determine whether this AccountSet is a normal or blocker transaction.
*
* Returns `Blocker` when the transaction sets or clears `asfRequireAuth`,
* `asfDisableMaster`, or `asfAccountTxnID`, or when the legacy
* `tfRequireAuth`/`tfOptionalAuth` transaction flags are used. Blocker
* classification prevents later same-account transactions from being queued
* until this one confirms, guarding state-dependent sequences (e.g.,
* establishing trust lines before enabling RequireAuth).
*
* @param ctx Preflight context containing the raw transaction.
* @return A `TxConsequences` with `Category::Blocker` or `Category::Normal`.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/** Return the set of transaction flags valid for AccountSet.
*
* @param ctx Preflight context (unused; present for interface uniformity).
* @return `tfAccountSetMask`, causing any unknown flag bits to be rejected
* by the framework's `preflight1` call.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Perform stateless validation of AccountSet transaction fields.
*
* Runs without ledger access. Validates:
* - No flag is simultaneously set and cleared via the dual-path flag system
* (legacy `Flags` bitfield and `sfSetFlag`/`sfClearFlag` are both accepted
* but must not contradict each other for RequireAuth, RequireDestTag, and
* DisallowXRP).
* - `sfTransferRate`, if present, is zero (unset) or within
* [`QUALITY_ONE`, `2 × QUALITY_ONE`]. Values below `QUALITY_ONE` are
* rejected to prevent discount-rate value creation.
* - `sfTickSize`, if present, is zero (unset) or within
* [`Quality::kMIN_TICK_SIZE`, `Quality::kMAX_TICK_SIZE`].
* - `sfMessageKey`, if present and non-empty, is a recognized public key type.
* - `sfDomain`, if present, does not exceed `kMAX_DOMAIN_LENGTH`.
* - Setting `asfAuthorizedNFTokenMinter` requires `sfNFTokenMinter` present;
* clearing it requires `sfNFTokenMinter` absent.
*
* @param ctx Preflight context.
* @return `tesSUCCESS` on valid input, or a `tem*`/`tel*` code describing
* the first validation failure encountered.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Enforce the granular delegation model for delegate-signed AccountSet
* transactions.
*
* If no `sfDelegate` field is present the transaction passes immediately.
* Otherwise the method looks up the `DelegateObject` for `(account, delegate)`
* and rejects the transaction unless every modified field has been explicitly
* granted:
* - Any use of `sfSetFlag`, `sfClearFlag`, or the legacy `Flags` bitfield
* is categorically rejected — behavioral flag changes cannot be delegated.
* - `sfWalletLocator` and `sfNFTokenMinter` are unconditionally blocked.
* - `sfEmailHash`, `sfMessageKey`, `sfDomain`, `sfTransferRate`, and
* `sfTickSize` are permitted only when the corresponding granular permission
* constant (`AccountEmailHashSet`, `AccountMessageKeySet`, etc.) is present
* in the delegate's permission list.
*
* @param view Read-only ledger view used to locate the `DelegateObject`.
* @param tx The AccountSet transaction being evaluated.
* @return `tesSUCCESS` if delegation is absent or fully authorized;
* `terNO_DELEGATE_PERMISSION` if the delegate object is missing or any
* required granular permission has not been granted.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
/** Perform read-only ledger-state checks that may still reject the transaction.
*
* Two state-dependent constraints are enforced here:
* - **RequireAuth**: enabling `asfRequireAuth` when the account's owner
* directory is non-empty is rejected (`tecOWNERS` or `terOWNERS` when
* `tapRetry` is set), preventing retroactive breakage of existing trust
* relationships.
* - **Clawback / NoFreeze mutual exclusion** (when `featureClawback` is
* active): `asfAllowTrustLineClawback` cannot be set if `lsfNoFreeze` is
* already set, and vice versa. Enabling clawback also requires an empty
* owner directory for the same retroactive-breakage reason.
*
* @param ctx Preclaim context providing a read-only ledger view and the
* transaction.
* @return `tesSUCCESS` on success; `tecOWNERS`, `terOWNERS`, or
* `tecNO_PERMISSION` when the relevant constraint is violated;
* `terNO_ACCOUNT` if the account root does not exist.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the AccountSet transaction, mutating the account root SLE.
*
* Reads the current `sfFlags` bitmask, computes the new value by applying
* each flag change in sequence (both legacy-bitfield and `sfSetFlag`/
* `sfClearFlag` paths), updates scalar metadata fields, and calls
* `ctx_.view().update(sle)` once at the end.
*
* Security-sensitive flag handling:
* - **DisableMaster**: requires the transaction to be signed with the master
* key itself and the account to already have an alternative signing path
* (`sfRegularKey` or a signer list); returns `tecNEED_MASTER_KEY` or
* `tecNO_ALTERNATIVE_KEY` otherwise.
* - **NoFreeze**: likewise requires a master-key signature when master is
* still enabled. `asfNoFreeze` is permanent — the flag can never be
* cleared.
* - **GlobalFreeze interlock**: once `lsfNoFreeze` is active, clearing
* `lsfGlobalFreeze` is prohibited to prevent selective market manipulation.
*
* Scalar fields (`sfDomain`, `sfEmailHash`, `sfMessageKey`, `sfWalletLocator`,
* `sfTransferRate`, `sfTickSize`) use an empty or zero value as a deletion
* signal (`makeFieldAbsent`) rather than a separate clear flag.
*
* Amendment-gated flags (`asfAllowTrustLineLocking` via `featureTokenEscrow`,
* `asfAllowTrustLineClawback` via `featureClawback`) are only applied when
* the corresponding amendment is active.
*
* @return `tesSUCCESS` on success; `tecNEED_MASTER_KEY` if a master-key
* operation was attempted without a master-key signature;
* `tecNO_ALTERNATIVE_KEY` if DisableMaster was requested with no fallback
* signing path; `tefINTERNAL` if the account SLE cannot be located
* (indicates ledger corruption, expected unreachable).
*/
TER
doApply() override;
/** Per-entry hook for AccountSet-specific invariant checking.
*
* Currently a no-op placeholder; AccountSet has no transaction-specific
* invariants beyond the global set.
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize AccountSet-specific invariant checks after all entries are visited.
*
* Currently a no-op placeholder; always returns `true`.
*
* @param tx The AccountSet transaction.
* @param result The TER result from `doApply`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the ledger after application.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally (no AccountSet-specific invariants yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,30 +4,105 @@
namespace xrpl {
/** Transactor for the SetRegularKey transaction type.
*
* Assigns or revokes an account's regular key — a secondary keypair that may
* sign transactions in place of the master key. Separating signing authority
* from account ownership lets accounts keep their master key in cold storage
* while using the regular key for day-to-day operations.
*
* Declared as `Blocker` so that no other transaction from the same account
* may be queued ahead of or alongside it: changing the signing key would
* invalidate the signatures of any concurrently queued transactions.
*
* @see AccountSet, SignerListSet
*/
class SetRegularKey : public Transactor
{
public:
/** Marks this transactor as a queue blocker.
*
* A Blocker transaction prevents later transactions from the same account
* from being queued until this one settles, because a key change can
* invalidate the signatures of any pending transactions.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Blocker;
explicit SetRegularKey(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Stateless validity check run before any ledger access.
*
* Rejects the transaction with `temBAD_REGKEY` if `sfRegularKey` equals
* `sfAccount` — assigning the account's own ID as its regular key is a
* degenerate no-op and almost certainly a mistake.
*
* @param ctx The preflight context carrying the raw transaction.
* @return `temBAD_REGKEY` if `sfRegularKey == sfAccount`; otherwise
* `tesSUCCESS`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Computes the base fee, waiving it for a first-time key rotation.
*
* If the transaction is signed with the account's own master public key
* and the account's `lsfPasswordSpent` flag is not yet set, returns
* `XRPAmount{0}` — making the transaction free. This bootstrapping
* mechanic lets a newly funded account replace an operator-assigned
* "password" key with its own key at no cost, lowering the barrier to
* entry. The waiver is consumed exactly once: `doApply` sets
* `lsfPasswordSpent` on the same code path.
*
* @param view Read-only ledger view used to inspect the account SLE.
* @param tx The transaction being evaluated.
* @return `XRPAmount{0}` when the one-time waiver applies; otherwise
* delegates to `Transactor::calculateBaseFee`.
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Applies the key change to the account SLE.
*
* **Setting a key**: writes `sfRegularKey` to the account SLE. If the
* fee-waiver path was used (i.e., `minimumFee` returns zero), also sets
* `lsfPasswordSpent` to prevent a second free rotation.
*
* **Removing a key** (`sfRegularKey` absent in the transaction): enforces
* the lockout-prevention invariant — if `lsfDisableMaster` is set and no
* multisig signer list exists, removing the regular key would leave the
* account with no valid signing path. Returns `tecNO_ALTERNATIVE_KEY` in
* that case. Otherwise clears `sfRegularKey` via `makeFieldAbsent`.
*
* @return `tesSUCCESS` on success; `tecNO_ALTERNATIVE_KEY` when removal
* would permanently lock the account; `tefINTERNAL` (unreachable in
* practice) if the account SLE cannot be found.
*/
TER
doApply() override;
/** Invariant visitor hook — no per-entry checks are defined yet.
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Invariant finalization hook — no transaction-specific invariants yet.
*
* @param tx The applied transaction.
* @param result The TER result of `doApply`.
* @param fee The fee charged.
* @param view Read-only view of the post-apply ledger.
* @param j Journal for diagnostic logging.
* @return Always `true`; reserved for future invariant checks.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -10,17 +10,40 @@
namespace xrpl {
/**
See the README.md for an overview of the SignerListSet transaction that
this class implements.
*/
/** Manages an account's multi-signature signer list on the XRP Ledger.
*
* Handles three operations determined entirely by the transaction fields:
* creating or replacing a signer list (`sfSignerQuorum` non-zero +
* `sfSignerEntries` present), or destroying the existing list (`sfSignerQuorum`
* zero + `sfSignerEntries` absent). Any other combination is `temMALFORMED`.
*
* `kCONSEQUENCES_FACTORY` is `Blocker`: a queued `SignerListSet` prevents
* later transactions from the same account from being applied until this one
* resolves, because changing signing authority has security implications that
* require serialized processing.
*
* @note Signer accounts are not required to exist in the ledger; XRPL
* permits "phantom accounts" as signers.
* @see AccountDelete, which calls `removeFromLedger()` during account cleanup.
*/
class SignerListSet : public Transactor
{
private:
// Values determined during preCompute for use later.
enum class Operation { Unknown, Set, Destroy };
/** Identifies which ledger mutation `doApply()` will perform. */
enum class Operation {
Unknown, /**< Malformed transaction; `doApply()` will not be reached. */
Set, /**< Create or replace the signer list. */
Destroy /**< Remove the signer list entirely. */
};
/** Cached operation type, populated by `preCompute()`. */
Operation do_{Operation::Unknown};
/** Cached quorum value from the transaction, populated by `preCompute()`. */
std::uint32_t quorum_{0};
/** Cached deserialized signer entries, sorted for duplicate detection,
* populated by `preCompute()`. */
std::vector<SignerEntries::SignerEntry> signers_;
public:
@@ -30,23 +53,66 @@ public:
{
}
/** Returns the valid transaction flags mask.
*
* Returns `tfUniversalMask` when `fixInvalidTxFlags` is active so that
* unknown flag bits are rejected. Returns `0` (allow any flags) on older
* rule sets to preserve backward compatibility.
*
* @param ctx Preflight context carrying the current rule set.
* @return The flags mask to enforce, or `0` to allow all flags.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless validation of the transaction fields.
*
* Determines the intended operation via `determineOperation()` and, for
* `Set` operations, delegates to `validateQuorumAndSignerEntries()` to
* enforce signer count bounds, uniqueness, self-reference, positive
* weights, and quorum reachability. No ledger access is performed.
*
* @param ctx Preflight context.
* @return `tesSUCCESS` if the transaction is well-formed; `temMALFORMED`
* for an invalid quorum/entries combination; `temBAD_SIGNER`,
* `temBAD_WEIGHT`, or `temBAD_QUORUM` for specific signer list
* violations.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Executes the signer list operation against the ledger.
*
* Dispatches to `replaceSignerList()` or `destroySignerList()` based on
* the `do_` value set by `preCompute()`. The `Unknown` branch is
* unreachable in practice because `preflight()` already rejected it.
*
* @return `tesSUCCESS` on success, or a `tec`/`tef` code on failure.
*/
TER
doApply() override;
/** Parses and caches operation parameters before `doApply()` runs.
*
* Calls `determineOperation()` and stores the resulting `quorum_`,
* `signers_`, and `do_` for use in `doApply()`, avoiding a second parse
* of the transaction fields. Asserts that the operation is well-formed
* (preflight must have already validated it).
*/
void
preCompute() override;
/** No-op: no transaction-specific invariant entries to visit yet. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op: no transaction-specific invariants to finalize yet.
*
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -55,7 +121,22 @@ public:
ReadView const& view,
beast::Journal const& j) override;
// Interface used by AccountDelete
/** Removes the account's signer list from the ledger without constructing
* a `SignerListSet` instance.
*
* Called by `AccountDelete` during account deletion to clean up the
* owned `ltSIGNER_LIST` entry. Adjusts the owner count to match the
* pre- or post-`MultiSignReserve` accounting based on the `lsfOneOwnerCount`
* flag on the existing SLE. If no signer list exists, returns `tesSUCCESS`
* immediately.
*
* @param registry Service registry used to obtain a journal.
* @param view Mutable ledger view.
* @param account The account whose signer list is to be removed.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on success; `tefBAD_LEDGER` if the owner directory
* entry cannot be removed (indicates ledger corruption).
*/
static TER
removeFromLedger(
ServiceRegistry& registry,
@@ -64,9 +145,40 @@ public:
beast::Journal j);
private:
/** Parses the transaction to determine the intended operation.
*
* A non-zero `sfSignerQuorum` combined with `sfSignerEntries` present
* maps to `Operation::Set`; zero quorum with no entries maps to
* `Operation::Destroy`; any other combination leaves the operation as
* `Operation::Unknown`. The returned signer list is sorted to enable
* O(n) duplicate detection via `std::adjacent_find`.
*
* @param tx The transaction to inspect.
* @param flags Apply-phase flags.
* @param j Journal for diagnostic logging.
* @return A tuple of `(NotTEC, quorum, signers, Operation)`. The `NotTEC`
* is non-success only if `SignerEntries::deserialize` fails.
*/
static std::tuple<NotTEC, std::uint32_t, std::vector<SignerEntries::SignerEntry>, Operation>
determineOperation(STTx const& tx, ApplyFlags flags, beast::Journal j);
/** Validates signer list semantics for a `Set` operation.
*
* Enforces: signer count in `[kMIN_MULTI_SIGNERS, kMAX_MULTI_SIGNERS]`
* (currently 132); no duplicates (requires `signers` to be sorted);
* no self-referencing signer; every weight positive; and total weight
* reachable by at least one combination (sum ≥ quorum).
*
* @param quorum The quorum threshold from `sfSignerQuorum`.
* @param signers The sorted, deserialized signer list.
* @param account The submitting account; no signer may reference it.
* @param j Journal for diagnostic logging.
* @param rules Current ledger rules.
* @return `tesSUCCESS`, `temMALFORMED`, `temBAD_SIGNER`, `temBAD_WEIGHT`,
* or `temBAD_QUORUM`.
* @note Signer accounts are not checked for ledger existence; phantom
* accounts are intentionally permitted.
*/
static NotTEC
validateQuorumAndSignerEntries(
std::uint32_t quorum,
@@ -75,11 +187,41 @@ private:
beast::Journal j,
Rules const&);
/** Creates or replaces the signer list in the ledger.
*
* Removes any existing `ltSIGNER_LIST` first (which may lower the owner
* count and ease the subsequent reserve check), then inserts the new
* entry with `lsfOneOwnerCount` set, consuming exactly 1 owner-count
* unit under the `MultiSignReserve` amendment. Reserve is checked against
* `preFeeBalance_` to allow fee payment from reserve.
*
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE`, or `tecDIR_FULL`.
*/
TER
replaceSignerList();
/** Removes the signer list from the ledger.
*
* Rejects with `tecNO_ALTERNATIVE_KEY` if the master key is disabled
* (`lsfDisableMaster`) and no `sfRegularKey` is set, which would leave
* the account with no signing mechanism.
*
* @return `tesSUCCESS`, `tecNO_ALTERNATIVE_KEY`, or `tefBAD_LEDGER`.
*/
TER
destroySignerList();
/** Serializes the cached signer list into a ledger SLE.
*
* Writes `sfSignerQuorum`, `sfSignerListID` (always 0), and `sfFlags`.
* Conditionally writes `sfOwner` only when `fixIncludeKeyletFields` is
* active. Each signer entry's `sfWalletLocator` (tag) is written only
* when a tag is present, ensuring no spurious tag field is ever committed
* to the ledger.
*
* @param ledgerEntry The SLE being populated.
* @param flags `sfFlags` value to write; skipped if zero.
*/
void
writeSignersToSLE(SLE::pointer const& ledgerEntry, std::uint32_t flags) const;
};

View File

@@ -1,3 +1,16 @@
/** @file
* Declares all eight transactors implementing the XRPL cross-chain bridge
* protocol (XLS-38d).
*
* The bridge protocol enables value transfers between two independent XRPL
* ledgers via a three-phase pipeline common to all XRPL transactors:
* stateless `preflight` validation, read-only `preclaim` ledger checks, and
* state-mutating `doApply` execution.
*
* @see XChainAttestations.h for the cryptographic witness structures consumed
* by `XChainAddClaimAttestation` and `XChainAddAccountCreateAttestation`.
*/
#pragma once
#include <xrpl/protocol/XChainAttestations.h>
@@ -5,10 +18,34 @@
namespace xrpl {
/** Maximum number of pending account-creation claims in the destination-chain
* queue.
*
* Account-create commits are processed strictly in source-chain sequence order.
* This cap bounds the queue depth — and therefore the maximum processing lag —
* for pending account-creation attestations. If the queue is full, new
* `XChainCreateAccountCommit` transactions are rejected until earlier entries
* are finalized.
*/
constexpr size_t kXBRIDGE_MAX_ACCOUNT_CREATE_CLAIMS = 128;
// Attach a new bridge to a door account. Once this is done, the cross-chain
// transfer transactions may be used to transfer funds from this account.
/** Attaches a new bridge definition to a "door account" on one side of a
* cross-chain bridge, establishing it as the custody point for locked assets.
*
* This is a one-time setup transaction. After it succeeds, the door account
* anchors all subsequent cross-chain activity on its chain: `XChainCommit`
* locks funds into it, witness servers attest to events on its behalf, and
* `XChainClaim` releases funds from it. Both a locking-chain door and an
* issuing-chain door must be configured (on their respective chains) before
* transfers can flow.
*
* @note `preflight` enforces that the transaction sender must be one of the
* two door accounts specified in the bridge definition, that both sides
* carry the same asset type (XRP or a specific IOU), and that the reward
* amount is a non-negative XRP value. `preclaim` rejects if a bridge
* already exists on this account or, for IOU bridges, if the issuer
* account does not exist or has clawback enabled.
*/
class XChainCreateBridge : public Transactor
{
public:
@@ -18,21 +55,52 @@ public:
{
}
/** Validates the bridge specification before any ledger access.
*
* Checks that: the transaction sender is one of the two declared door
* accounts; the two door accounts are distinct; both sides of the bridge
* carry the same asset type; and the reward amount is a non-negative XRP
* value.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Checks ledger state before committing the bridge creation.
*
* Verifies that no bridge object already exists for this account, that
* the submitting account has sufficient reserve for the new ledger entry,
* and — for IOU bridges — that the issuer account exists and has not
* enabled clawback.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the ledger state permits creation; a `tec*`
* code otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Creates the bridge ledger object and adds it to the owner directory.
*
* Initialises claim ID and account-creation counters to zero, inserts the
* new `ltBRIDGE` SLE, and increments the door account's owner count.
*
* @return `tesSUCCESS` on success; a `tec*` code if state has changed
* since preclaim (e.g., reserve no longer satisfied).
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -42,6 +110,18 @@ public:
beast::Journal const& j) override;
};
/** Modifies parameters of an existing bridge object owned by a door account.
*
* At least one of the following must be specified: a new witness-reward
* amount, a new `MinAccountCreateAmount`, or the `tfClearAccountCreateAmount`
* flag (which removes the field and disables `XChainCreateAccountCommit` on
* that bridge). Setting and clearing `MinAccountCreateAmount` in the same
* transaction is rejected.
*
* Unlike most transactors, `BridgeModify` overrides `getFlagsMask()` because
* bridge modification supports flag-driven parameter toggles that require a
* custom mask (`tfXChainModifyBridgeMask`).
*/
class BridgeModify : public Transactor
{
public:
@@ -51,24 +131,60 @@ public:
{
}
/** Returns the set of transaction flags valid for bridge modification.
*
* Overrides the base-class default to return `tfXChainModifyBridgeMask`,
* which includes the `tfClearAccountCreateAmount` toggle flag not present
* in the common flag set.
*
* @param ctx Preflight context (unused; present for interface conformance).
* @return Bitmask of permitted transaction flags.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validates modification parameters before any ledger access.
*
* Rejects if: no modifiable field is present; both `sfMinAccountCreateAmount`
* and `tfClearAccountCreateAmount` are specified simultaneously; the
* transaction sender is not one of the bridge's door accounts; or any
* supplied amount is invalid.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Checks that the target bridge exists before applying modifications.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the bridge ledger entry exists; `tecNO_ENTRY`
* otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Applies the requested parameter changes to the bridge ledger object.
*
* Updates `sfSignatureReward` and/or `sfMinAccountCreateAmount` in-place,
* or removes `sfMinAccountCreateAmount` when `tfClearAccountCreateAmount`
* is set.
*
* @return `tesSUCCESS` on success; a `tec*` code if the bridge no longer
* exists when doApply runs.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -78,44 +194,86 @@ public:
beast::Journal const& j) override;
};
/** Alias for `BridgeModify` using the canonical XRPL transaction-type name. */
using XChainModifyBridge = BridgeModify;
//------------------------------------------------------------------------------
// Claim funds from a `XChainCommit` transaction. This is normally not needed,
// but may be used to handle transaction failures or if the destination account
// was not specified in the `XChainCommit` transaction. It may only be used
// after a quorum of signatures have been sent from the witness servers.
//
// If the transaction succeeds in moving funds, the referenced `XChainClaimID`
// ledger object will be destroyed. This prevents transaction replay. If the
// transaction fails, the `XChainClaimID` will not be destroyed and the
// transaction may be re-run with different parameters.
/** Releases funds on the destination chain that were locked by `XChainCommit`.
*
* This is step 4 of the normal cross-chain transfer sequence. It is required
* when no destination account was embedded in the original `XChainCommit`, or
* when the embedded destination failed (e.g., deposit auth rejected). It can
* only execute after a quorum of witness-server attestations has been
* accumulated against the referenced `XChainClaimID`.
*
* On success the `XChainClaimID` ledger object is destroyed, preventing
* replay. On failure the claim ID survives, allowing the transaction to be
* resubmitted with corrected parameters (e.g., a different destination or
* amount). This recovery path distinguishes `XChainClaim` from the
* account-creation flow, which has no undo mechanism.
*
* `kCONSEQUENCES_FACTORY` is `Blocker` because whether attestations have
* already reached quorum — and therefore whether this transaction will move
* funds — cannot be determined at fee-calculation time.
*/
class XChainClaim : public Transactor
{
public:
// Blocker since we cannot accurately calculate the consequences
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Blocker;
explicit XChainClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Validates the claim request before any ledger access.
*
* Checks that the claimed amount is positive and matches one of the
* bridge's configured asset types.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates ledger state required for a successful claim.
*
* Verifies that: the referenced bridge exists; the destination account
* exists; the claimed amount matches the bridge's expected issue for this
* chain; and the `XChainClaimID` exists and is owned by the transaction
* sender.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all preconditions are met; a `tec*` code
* otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Executes the fund release on the destination chain.
*
* Retrieves the door account's signer list and quorum, checks that
* attestations on the claim ID meet quorum, transfers funds to the
* destination (bypassing deposit auth for the claim owner), distributes
* witness rewards, and destroys the `XChainClaimID` on success.
*
* @return `tesSUCCESS` on successful fund transfer; a `tec*` code if
* quorum is not yet reached, the destination rejects the payment, or
* the claim ID is no longer valid. On failure the claim ID is
* preserved for retry.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -127,14 +285,38 @@ public:
//------------------------------------------------------------------------------
// Put assets into trust on the locking-chain so they may be wrapped on the
// issuing-chain, or return wrapped assets on the issuing-chain so they can be
// unlocked on the locking-chain. The second step in a cross-chain transfer.
/** Locks assets on the source chain to initiate a cross-chain transfer.
*
* This is step 2 of the normal cross-chain transfer sequence. On the locking
* chain it places XRP or IOU assets into the door account's custody. On the
* issuing chain it returns wrapped assets to the door account so they can be
* released on the locking chain. The transaction must reference a claim ID
* previously created on the *destination* chain via `XChainCreateClaimID`.
*
* `kCONSEQUENCES_FACTORY` is `Custom` because the committed amount must be
* precisely reflected in consequence calculations: committing XRP effectively
* removes it from the sender's spendable balance, which the `Normal` factory
* cannot model.
*
* @note The transaction sender must not be the door account; a door account
* cannot commit funds to itself. The committed amount must match the
* bridge's configured asset for the source chain.
*/
class XChainCommit : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Custom;
/** Computes transaction consequences reflecting the committed amount.
*
* Returns the XRP value of the committed funds as the maximum spend so
* that the transaction queue can accurately model the sender's balance
* impact. Returns zero for non-XRP asset commits.
*
* @param ctx Preflight context providing the transaction fields.
* @return A `TxConsequences` object encoding the XRP spend (or zero for
* IOU commits).
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
@@ -142,21 +324,50 @@ public:
{
}
/** Validates the commit parameters before any ledger access.
*
* Checks that the committed amount is positive, legal, and matches one of
* the bridge's configured asset types.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates ledger state required for a successful commit.
*
* Verifies that: the referenced bridge exists; the sender is not the door
* account; and the committed amount matches the bridge's expected asset
* for this chain direction.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all preconditions are met; a `tec*` code
* otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Transfers the committed assets to the door account.
*
* Moves funds from the sender to the door account via `transferHelper`,
* which enforces deposit-auth and destination-tag checks. The sender is
* permitted to dip into reserve to cover the transaction fee.
*
* @return `tesSUCCESS` on successful transfer; a `tec*` code (e.g.,
* `tecUNFUNDED_PAYMENT`) if the sender has insufficient balance.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -168,14 +379,25 @@ public:
//------------------------------------------------------------------------------
// Create a new claim id owned by the account. This is the first step in a
// cross-chain transfer. The claim id must be created on the destination chain
// before the `XChainCommit` transaction (which must reference this number) can
// be sent on the source chain. The account that will send the `XChainCommit` on
// the source chain must be specified in this transaction (see note on the
// `SourceAccount` field in the `XChainClaimID` ledger object for
// justification). The actual sequence number must be retrieved from a validated
// ledger.
/** Reserves a claim ID on the destination chain — the first step in a normal
* cross-chain transfer.
*
* The claim ID must be created on the *destination* chain before
* `XChainCommit` can be submitted on the source chain, because the commit
* transaction must reference this monotonically-increasing sequence number.
* The account that will later send `XChainCommit` on the source chain must be
* bound here via `sfOtherChainSource`; this binding is the primary anti-replay
* mechanism and prevents an attacker from substituting a different sender
* after funds are locked.
*
* The actual sequence number assigned to the new claim ID must be retrieved
* from a validated ledger after this transaction closes.
*
* @note `preflight` validates only that `sfSignatureReward` is a non-negative
* XRP amount. `preclaim` additionally checks that the reward matches the
* bridge's configured reward and that the creator has sufficient reserve
* for the new `XChainClaimID` ledger object.
*/
class XChainCreateClaimID : public Transactor
{
public:
@@ -185,21 +407,51 @@ public:
{
}
/** Validates the claim ID creation request before any ledger access.
*
* Checks that `sfSignatureReward` is a non-negative XRP amount.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates ledger state required to create a claim ID.
*
* Verifies that the referenced bridge exists, that the specified signature
* reward matches the bridge's configured reward, and that the creating
* account has sufficient reserve for the new ledger entry.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all preconditions are met; `tecNO_ENTRY` if the
* bridge does not exist; `tecXCHAIN_REWARD_MISMATCH` if the reward
* does not match; `tecINSUFFICIENT_RESERVE` if reserve is too low.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Creates the `XChainClaimID` ledger object and adds it to the owner
* directory.
*
* Atomically increments the bridge's claim ID counter, initialises the
* new SLE with an empty attestations array and the bound source account,
* and increments the creator's owner count.
*
* @return `tesSUCCESS` on success; a `tec*` code if the bridge no longer
* exists or reserve has changed since preclaim.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -211,38 +463,82 @@ public:
//------------------------------------------------------------------------------
// Provide attestations from a witness server attesting to events on
// the other chain. The signatures must be from one of the keys on the door's
// signer's list at the time the signature was provided. However, if the
// signature list changes between the time the signature was submitted and the
// quorum is reached, the new signature set is used and some of the currently
// collected signatures may be removed. Also note the reward is only sent to
// accounts that have keys on the current list.
/** Submits a witness-server attestation for a regular cross-chain claim.
*
* This is step 3 of the normal cross-chain transfer sequence. Each off-chain
* witness server calls this transaction once to add its cryptographic
* signature to the `XChainClaimID` on the destination chain. Attestations
* accumulate until the door account's signer-list quorum is reached, at which
* point funds are automatically transferred and rewards are distributed.
*
* Signatures must originate from a key on the door account's signer list at
* the time the attestation is submitted. If the signer list is updated between
* submission and quorum, the new list governs: stale attestations may be
* removed and only attesters whose keys are on the *current* list receive
* rewards.
*
* `kCONSEQUENCES_FACTORY` is `Blocker` because it is not determinable at
* fee-calculation time whether this attestation will push the running count
* past quorum and trigger an immediate fund transfer.
*/
class XChainAddClaimAttestation : public Transactor
{
public:
// Blocker since we cannot accurately calculate the consequences
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Blocker;
explicit XChainAddClaimAttestation(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Validates the attestation before any ledger access.
*
* Verifies that the embedded `AttestationClaim` is well-formed: the
* public key format is valid, the signature over the claim data is
* cryptographically correct, and the attested amounts are legal.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid attestation; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates that the attesting key is authorised by the bridge.
*
* Confirms the bridge exists and that the attesting public key and its
* corresponding account appear in the door account's signer list.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the key is authorised; `tecXCHAIN_NO_SIGNERS_LIST`
* if the door account has no multi-sign list; `tecNO_ENTRY` if the
* bridge does not exist.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Adds the attestation to the claim ID and triggers transfer if quorum
* is reached.
*
* Loads the `XChainClaimID` SLE, appends the new `AttestationClaim`,
* reconciles against the current signer list (removing any stale entries),
* and — if the updated set meets quorum — calls `finalizeClaimHelper` to
* transfer funds and distribute rewards. Destroys the claim ID on
* successful transfer.
*
* @return `tesSUCCESS` on success; a `tec*` code if the claim ID does
* not exist, the amount mismatches, or the destination account cannot
* receive funds.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -252,31 +548,82 @@ public:
beast::Journal const& j) override;
};
/** Submits a witness-server attestation for a cross-chain account-creation
* claim.
*
* Mirrors `XChainAddClaimAttestation` but operates on the account-creation
* flow governed by `XChainCreateAccountCommit`. Attestations for account
* creation are processed in strict source-chain sequence order (by
* `createCount`); an attestation is only eligible to trigger execution once
* its sequence position is the next expected one.
*
* When quorum is reached and the sequence position is current, the attested
* XRP is credited to the destination account (creating it if it does not yet
* exist). The claim entry is then removed via `OnTransferFail::RemoveClaim`
* to prevent a failed or stale attestation from permanently blocking all
* subsequent account-creation claims.
*
* `kCONSEQUENCES_FACTORY` is `Blocker` for the same reason as
* `XChainAddClaimAttestation`: the impact of crossing quorum cannot be
* predicted at fee-calculation time.
*/
class XChainAddAccountCreateAttestation : public Transactor
{
public:
// Blocker since we cannot accurately calculate the consequences
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Blocker;
explicit XChainAddAccountCreateAttestation(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Validates the account-creation attestation before any ledger access.
*
* Verifies that the embedded `AttestationCreateAccount` is well-formed:
* the public key format is valid and the signature over the account-
* creation event data is cryptographically correct.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid attestation; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates that the attesting key is authorised by the bridge.
*
* Same checks as `XChainAddClaimAttestation::preclaim`: bridge must exist
* and the attesting key/account pair must be in the door account's signer
* list.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if authorised; a `tec*` code otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Adds the attestation and executes account creation if quorum is reached
* at the expected sequence position.
*
* Loads or creates the `XChainOwnedCreateAccountClaimID` SLE, appends the
* new `AttestationCreateAccount`, and — if quorum is met and this entry's
* `createCount` matches the bridge's current counter — transfers the XRP
* amount to the destination account (creating it if necessary) and
* distributes rewards. The claim entry is always removed after quorum,
* regardless of transfer outcome, to unblock subsequent sequence entries.
*
* @return `tesSUCCESS` on success; a `tec*` code if the attestation is
* malformed, the bridge does not exist, or the transfer itself fails.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -288,29 +635,36 @@ public:
//------------------------------------------------------------------------------
// This is a special transaction used for creating accounts through a
// cross-chain transfer. A normal cross-chain transfer requires a "chain claim
// id" (which requires an existing account on the destination chain). One
// purpose of the "chain claim id" is to prevent transaction replay. For this
// transaction, we use a different mechanism: the accounts must be claimed on
// the destination chain in the same order that the `XChainCreateAccountCommit`
// transactions occurred on the source chain.
//
// This transaction can only be used for XRP to XRP bridges.
//
// IMPORTANT: This transaction should only be enabled if the witness
// attestations will be reliably delivered to the destination chain. If the
// signatures are not delivered (for example, the chain relies on user wallets
// to collect signatures) then account creation would be blocked for all
// transactions that happened after the one waiting on attestations. This could
// be used maliciously. To disable this transaction on XRP to XRP bridges, the
// bridge's `MinAccountCreateAmount` should not be present.
//
// Note: If this account already exists, the XRP is transferred to the existing
// account. However, note that unlike the `XChainCommit` transaction, there is
// no error handling mechanism. If the claim transaction fails, there is no
// mechanism for refunds. The funds are permanently lost. This transaction
// should still only be used for account creation.
/** Commits XRP on the source chain to bootstrap account creation on the
* destination chain.
*
* Addresses the bootstrapping problem: a user who has no destination-chain
* account cannot call `XChainCreateClaimID`, because that requires an
* existing account. This transaction substitutes ordering-based replay
* prevention: commits are processed on the destination chain in strict
* source-chain sequence (enforced by the `createCount` counter), rather than
* via single-use claim ID objects.
*
* **Restricted to XRP-to-XRP bridges only.** The bridge's
* `sfMinAccountCreateAmount` field both sets a minimum commit size and acts
* as a feature gate: if the field is absent, this transaction type is
* disabled on that bridge.
*
* **⚠ Operational hazard — no error recovery.** If any attestation in the
* ordered sequence is not delivered to the destination chain, all subsequent
* account-creation claims are permanently blocked. There is no retry
* mechanism. The global cap `kXBRIDGE_MAX_ACCOUNT_CREATE_CLAIMS` limits
* queue depth but does not prevent this blockage.
*
* **⚠ Permanent loss on failure.** Unlike `XChainCommit` (whose claim ID
* survives a failed claim for retry), if the destination-chain finalisation
* fails the committed XRP is irrecoverably lost. This transaction should be
* used solely as an account-creation primitive, even if the destination
* account already exists.
*
* If the destination account already exists, the XRP is transferred to it
* rather than creating a new account.
*/
class XChainCreateAccountCommit : public Transactor
{
public:
@@ -320,21 +674,57 @@ public:
{
}
/** Validates the account-creation commit before any ledger access.
*
* Checks that both the committed amount and the witness reward are
* positive native XRP values and use the same asset type.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` on valid input; a `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validates ledger state required for the account-creation commit.
*
* Verifies that: the bridge exists; the specified reward matches the
* bridge's configured reward; `sfMinAccountCreateAmount` is set on the
* bridge (otherwise this transaction type is disabled); the committed
* amount is at least `sfMinAccountCreateAmount`; the bridge is XRP-to-XRP;
* and the sender is not the door account.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all preconditions are met; `tecXCHAIN_SELF_COMMIT`
* if the sender is the door account; `tecXCHAIN_INSUFF_CREATE_AMOUNT`
* if the amount is below the minimum; `tecNO_ENTRY` if the bridge does
* not exist or account creation is not enabled on it.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Transfers the committed XRP to the door account and increments the
* bridge's account-creation counter.
*
* Moves the committed amount plus the witness reward to the door account
* via `transferHelper`. The sender is permitted to dip into reserve to
* cover the transaction fee. Atomically increments `XChainAccountCreateCount`
* on the bridge SLE, establishing this commit's position in the
* destination-chain processing sequence.
*
* @return `tesSUCCESS` on success; `tecUNFUNDED_PAYMENT` if the sender
* has insufficient balance.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants */
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -344,6 +734,9 @@ public:
beast::Journal const& j) override;
};
/** Alias for `XChainCreateAccountCommit` using the canonical XRPL
* transaction-type name.
*/
using XChainAccountCreateCommit = XChainCreateAccountCommit;
//------------------------------------------------------------------------------

View File

@@ -1,33 +1,123 @@
/**
* @file
* @brief Transactor for the CheckCancel transaction type.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/**
* Removes a Check ledger object without transferring value.
*
* `CheckCancel` completes the Check lifecycle alongside `CheckCreate` (which
* writes the on-ledger object and reserves owner funds) and `CheckCash` (which
* redeems it). It is the only path for reclaiming the source account's owner
* reserve when a check goes unused.
*
* Permission model: if the check has **not** yet expired (tested against the
* parent ledger's close time), only the source account or the destination
* account may cancel it. An expired check may be removed by any account,
* allowing third-party cleanup of stale objects.
*
* `ConsequencesFactory` is `Normal` — no special blocking semantics. Unlike
* `CheckCreate` and `CheckCash`, this transactor does not override
* `checkExtraFeatures` because cancellation is always permitted regardless of
* active amendments.
*/
class CheckCancel : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Constructs a `CheckCancel` transactor bound to the given apply context. */
explicit CheckCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
/**
* Stateless validation phase — always succeeds.
*
* All meaningful validation (Check existence, canceller permission) is
* deferred to `preclaim`, which has read access to ledger state.
*
* @param ctx The preflight context (no ledger access).
* @return `tesSUCCESS` unconditionally.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/**
* Ledger-state validation phase.
*
* Resolves the Check object via `sfCheckID`. Enforces the permission rule:
* if the check has not expired, only the source or destination account may
* cancel it. Expiry is evaluated against the **parent** ledger's close time
* (the only definitively known close time at apply time).
*
* @param ctx The preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if the caller is permitted to cancel.
* @return `tecNO_ENTRY` if the Check object does not exist.
* @return `tecNO_PERMISSION` if the check is unexpired and the submitter
* is neither the source nor the destination account.
*/
static TER
preclaim(PreclaimContext const& ctx);
/**
* Commits the cancellation to the ledger.
*
* Performs four coordinated writes in order:
* 1. Removes the Check from the destination account's owner directory
* using the stored `sfDestinationNode` page index (skipped when source
* and destination are the same account).
* 2. Removes the Check from the source account's owner directory using
* the stored `sfOwnerNode` page index.
* 3. Decrements the source account's owner count by 1 via
* `adjustOwnerCount`, releasing the reserve that `CheckCreate` claimed.
* 4. Erases the Check SLE from the ledger.
*
* The two `dirRemove` calls are guarded by `tefBAD_LEDGER` paths
* (marked `LCOV_EXCL`) — the stored page indices are immutable after
* creation and `preclaim` has already confirmed the Check exists, so
* those branches represent ledger corruption rather than user error.
*
* @return `tesSUCCESS` on success.
* @return `tecNO_ENTRY` if the Check cannot be peeked (should not occur
* after a passing `preclaim`).
* @return `tefBAD_LEDGER` if a directory removal fails due to ledger
* corruption (unreachable in a correctly functioning ledger).
*/
TER
doApply() override;
/**
* Per-entry invariant visitor hook — no transaction-specific invariants
* are currently registered for `CheckCancel`.
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/**
* Post-transaction invariant finalizer — no transaction-specific
* invariants are currently registered for `CheckCancel`.
*
* @param tx The applied transaction.
* @param result The TER result from `doApply`.
* @param fee The fee deducted.
* @param view A read-only view of the resulting ledger state.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally (no invariants to check yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,36 +1,180 @@
/** @file
* Declares the CheckCash transactor for the XRP Ledger.
*
* `CheckCash` is the only check transactor that transfers value. The other
* two (`CheckCreate`, `CheckCancel`) manage the ledger object lifecycle
* without moving funds.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/** Executes a `ttCHECK_CASH` transaction, redeeming a Check for its value.
*
* A Check is an asynchronous, pull-based payment: the sender creates it via
* `CheckCreate`, authorizing the named destination to withdraw up to
* `sfSendMax` at any future time. `CheckCash` is how the destination collects
* those funds. After a successful cash, the Check ledger object is deleted
* and the source's owner reserve is released.
*
* The transaction accepts either `sfAmount` (exact delivery) or
* `sfDeliverMin` (deliver as much as possible, at least this minimum) —
* exactly one must be present. XRP transfers are handled directly; IOU and
* MPT transfers route through the `flow()` payment engine, which may
* auto-create a trust line or MPT holding on the destination if needed.
*
* `ConsequencesFactory` is `Normal` — the transaction claims its fee on
* failure and does not block other transactions in the sender's queue.
*
* @note MPT-denominated checks require the `featureMPTokensV2` amendment,
* enforced in `checkExtraFeatures` rather than `preflight`.
* @see CheckCreate, CheckCancel
*/
class CheckCash : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Constructs a `CheckCash` transactor bound to the given apply context. */
explicit CheckCash(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Amendment guard for MPT-denominated checks.
*
* Returns `false` (blocking the transaction) when either `sfAmount` or
* `sfDeliverMin` names an MPT asset and `featureMPTokensV2` is not yet
* active. IOU and XRP checks pass unconditionally.
*
* @param ctx The preflight context carrying the transaction and active
* rule set.
* @return `true` if the transaction may proceed to `preflight`; `false`
* if it should be rejected because the required amendment is not
* enabled.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Stateless structural validation.
*
* Enforces two invariants without accessing ledger state:
* 1. Exactly one of `sfAmount` or `sfDeliverMin` is present; both or
* neither is `temMALFORMED`.
* 2. The chosen amount passes `isLegalNet()`, is strictly positive, and
* does not name `badAsset()`.
*
* @param ctx The preflight context (no ledger access).
* @return `tesSUCCESS` if the transaction is structurally valid.
* @return `temMALFORMED` if both or neither amount field is present, or
* if the amount is zero, negative, or an illegal asset.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger-state validation.
*
* Resolves the Check via `sfCheckID` and verifies all preconditions
* without modifying state:
* - Check exists (`tecNO_ENTRY` otherwise).
* - Submitter is the check's destination (`tecNO_PERMISSION` otherwise).
* - Source and destination are different accounts (`tecINTERNAL` if not —
* this is a "should never happen" path guarded by `LCOV_EXCL`).
* - Destination tag is present if the destination account requires it.
* - Check has not expired (`tecEXPIRED` otherwise).
* - Requested asset and issuer match the check's `sfSendMax`; requested
* amount does not exceed `sfSendMax`.
* - Source has sufficient available funds. For XRP checks, one reserve
* increment is added to the available balance to account for the reserve
* that will be released when the Check is deleted.
* - For IOU assets not self-issued by the destination: issuer account
* exists, trust line authorization is satisfied, and the destination's
* trust line is not frozen.
* - For MPT assets: destination's holding passes `requireAuth` (weak),
* is not frozen (`isFrozen`), and the asset permits DEX trading
* (`canTrade`).
*
* @param ctx The preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if all conditions are met.
* @return `tecNO_ENTRY` if the Check object does not exist.
* @return `tecNO_PERMISSION` if the submitter is not the check's
* destination.
* @return `tecINTERNAL` if source and destination are the same account
* (ledger-corruption sentinel, should be unreachable).
* @return `tecEXPIRED` if the check has passed its expiration time.
* @return `tecINSUFFICIENT_FUNDS` if the source cannot cover the
* requested amount.
* @return `tecFROZEN` / `tecLOCKED` if the relevant trust line or MPT
* holding is frozen or locked.
* @return `tecNO_AUTH` if authorization is required but not granted.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Commits the check redemption to the ledger.
*
* All mutations occur inside a `PaymentSandbox`; `psb.apply()` is called
* only after every step succeeds, ensuring atomicity.
*
* **XRP path** — handled directly (not via `flow()`): `xrpLiquid()` is
* called with a `-1` reserve adjustment to account for the reserve
* released when the Check is deleted. For `sfDeliverMin`, delivery is
* `max(DeliverMin, min(sendMax, srcLiquid))`.
*
* **IOU/MPT path** — routed through `flow()`. If no trust line exists
* between the destination and the issuer, one is created automatically
* (the destination is signing, so their intent is unambiguous); its limit
* is temporarily raised to `cMaxValue` for the duration of the `flow()`
* call, then restored via `scope_exit`. For MPT assets without an
* existing holding, `checkCreateMPT()` initializes the slot. For
* `sfDeliverMin`, the ask amount passed to `flow()` is `cMaxValue / 2`
* to tolerate up to 200% gateway transfer rates without overflow.
*
* On success: the Check is removed from both the owner and destination
* account directories, the source's owner count is decremented, and the
* Check SLE is erased.
*
* @return `tesSUCCESS` on successful fund transfer and Check deletion.
* @return `tecNO_ENTRY` if the Check SLE cannot be peeked (should not
* occur after passing `preclaim`).
* @return `tecPATH_PARTIAL` if `sfDeliverMin` is specified and `flow()`
* cannot deliver the minimum amount.
* @return `tecPATH_DRY` if the payment path is exhausted.
* @return `tefBAD_LEDGER` if a directory removal fails due to ledger
* corruption (unreachable in a correctly functioning ledger).
*/
TER
doApply() override;
/** Per-entry invariant visitor hook.
*
* No transaction-specific invariants are currently registered for
* `CheckCash`; this is a no-op stub reserved for future use.
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant finalizer.
*
* No transaction-specific invariants are currently registered for
* `CheckCash`; always returns `true`. Reserved for future use.
*
* @param tx The applied transaction.
* @param result The TER result from `doApply`.
* @param fee The fee deducted.
* @param view A read-only view of the resulting ledger state.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,36 +1,175 @@
/** @file
* Declares the CheckCreate transactor for the XRP Ledger.
*
* A Check is a deferred payment authorization: the sender commits a
* `sfSendMax` amount to a named destination, which may later redeem it
* via `CheckCash` or cancel it via `CheckCancel`. This header defines the
* interface; the validation and ledger-mutation logic lives in
* `CheckCreate.cpp`.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/** Executes a `ttCHECK_CREATE` transaction, writing a Check object to the
* ledger.
*
* A Check is a pull-based payment authorization: the drawer (source account)
* specifies a destination and a maximum spend (`sfSendMax`), but no funds
* move at creation time. The destination may later redeem the Check via
* `CheckCash`, or either party may remove it via `CheckCancel`.
*
* Creating a Check adds one object to the sender's owner directory and
* increments their owner count, increasing their reserve requirement by one
* increment. The reserve is checked against `preFeeBalance_` so that the
* transaction fee may be paid from reserve funds while still requiring full
* reserve coverage for the new object.
*
* `kCONSEQUENCES_FACTORY` is `Normal` — the transaction claims its fee on
* failure and does not block other transactions in the sender's queue.
*
* @note MPT-denominated checks require the `featureMPTokensV2` amendment,
* enforced in `checkExtraFeatures` rather than in `preflight`.
* @see CheckCash, CheckCancel
*/
class CheckCreate : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Constructs a `CheckCreate` transactor bound to the given apply context. */
explicit CheckCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Amendment guard for MPT-denominated checks.
*
* Returns `false` (blocking the transaction) when `sfSendMax` names an
* MPT asset and `featureMPTokensV2` is not yet active. IOU and XRP checks
* pass unconditionally. This is the correct place for amendment gating,
* keeping feature checks separate from the core field validation in
* `preflight`.
*
* @param ctx The preflight context carrying the transaction and active
* rule set.
* @return `true` if the transaction may proceed to `preflight`; `false`
* if it should be rejected because the required amendment is not
* enabled.
*/
static bool
checkExtraFeatures(xrpl::PreflightContext const& ctx);
/** Stateless structural validation.
*
* Validates the transaction fields without accessing ledger state:
* - Rejects self-addressed checks (`sfAccount == sfDestination`) with
* `temREDUNDANT`.
* - Rejects `sfSendMax` that fails `isLegalNet()`, is non-positive, or
* names `badAsset()`.
* - Rejects a zero `sfExpiration` value; a non-zero value means "expires
* at that time", while absence means "never expires".
*
* @param ctx The preflight context (no ledger access).
* @return `tesSUCCESS` if the transaction is structurally valid.
* @return `temREDUNDANT` if the source and destination accounts are
* identical.
* @return `temBAD_AMOUNT` if `sfSendMax` is zero, negative, or otherwise
* malformed.
* @return `temBAD_CURRENCY` if `sfSendMax` names `badAsset()`.
* @return `temBAD_EXPIRATION` if `sfExpiration` is present and zero.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger-state validation.
*
* Checks all preconditions that require ledger access:
* - Destination account exists.
* - Destination has not set `lsfDisallowIncomingCheck`.
* - Destination is not a pseudo-account (not amendment-gated; the
* pseudo-account discriminator fields are themselves amendment-gated).
* - Destination tag is present when `lsfRequireDestTag` is set on the
* destination.
* - For non-XRP `sfSendMax`: the asset is not globally frozen; IOU
* trustlines from sender to issuer and from issuer to destination are
* not individually frozen; MPT holdings for sender and destination (when
* neither is the issuer) are not locked. A missing trustline on the
* sender side is permitted — the check is speculative and the trustline
* need not exist at creation time.
* - `sfExpiration`, if present, has not already passed.
* - The asset is tradeable via `canTrade`.
*
* @param ctx The preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if all conditions are met.
* @return `tecNO_DST` if the destination account does not exist.
* @return `tecNO_PERMISSION` if the destination has set
* `lsfDisallowIncomingCheck` or is a pseudo-account.
* @return `tecDST_TAG_NEEDED` if the destination requires a destination
* tag and none was supplied.
* @return `tecFROZEN` if an IOU trustline (global or per-line) is frozen.
* @return `tecLOCKED` if the MPT asset or an MPT holding is locked.
* @return `tecEXPIRED` if the specified expiration has already passed.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Commits the Check creation to the ledger.
*
* Enforces the reserve requirement against `preFeeBalance_` (the
* pre-fee balance captured by the base class), deliberately allowing the
* transaction fee to be paid from reserve funds while still requiring full
* reserve coverage for the new Check object.
*
* Constructs the Check SLE keyed by `keylet::check(account_, seq)`, where
* `seq` is the transaction's sequence or ticket value. Populates all
* optional fields (`sfSourceTag`, `sfDestinationTag`, `sfInvoiceID`,
* `sfExpiration`) only when present in the transaction. The Check is
* inserted into both the sender's and destination's owner directories;
* `sfOwnerNode` and `sfDestinationNode` record the respective directory
* page indices for O(1) removal. On success, `adjustOwnerCount`
* increments the sender's owner count by 1.
*
* @return `tesSUCCESS` on successful Check creation.
* @return `tecINSUFFICIENT_RESERVE` if the sender's pre-fee balance
* cannot cover the incremented reserve requirement.
* @return `tecDIR_FULL` if either owner directory has no room for a new
* entry (ledger-corruption sentinel, marked `LCOV_EXCL`).
* @return `tefINTERNAL` if the sender's `AccountRoot` SLE cannot be
* peeked (ledger-corruption sentinel, marked `LCOV_EXCL`).
*/
TER
doApply() override;
/** Per-entry invariant visitor hook.
*
* No transaction-specific invariants are currently registered for
* `CheckCreate`; this is a no-op stub reserved for future use.
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant finalizer.
*
* No transaction-specific invariants are currently registered for
* `CheckCreate`; always returns `true`. Reserved for future use.
*
* @param tx The applied transaction.
* @param result The TER result from `doApply`.
* @param fee The fee deducted.
* @param view A read-only view of the resulting ledger state.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,33 +4,107 @@
namespace xrpl {
/** Transactor for the CredentialAccept transaction type.
*
* Implements the subject-side acceptance step of the two-phase credential
* issuance lifecycle. After an issuer creates a credential directed at a
* subject via `CredentialCreate`, the subject must submit a `CredentialAccept`
* transaction to activate it. Only accepted credentials are usable in
* permission-gated operations.
*
* On acceptance the reserve burden shifts atomically from issuer to subject:
* the issuer's owner count decrements by 1 and the subject's increments by 1.
* If the credential has already expired at apply time it is deleted and
* `tecEXPIRED` is returned, even though the accepting transaction itself
* fails.
*/
class CredentialAccept : public Transactor
{
public:
/** Standard consequence semantics: consumes a sequence number, no blocking
* or custom consequence logic.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Construct a CredentialAccept transactor for the given apply context. */
explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Return the set of flags permitted on this transaction type.
*
* When `fixInvalidTxFlags` is active, restricts accepted flags to
* `tfUniversalMask`, causing any unknown flag bits to be rejected as
* malformed. Returns `0` (allow any flags) on pre-fix ledgers to preserve
* backward compatibility.
*
* @param ctx Preflight context carrying the current rule set.
* @return Bitmask of valid non-universal flags, or `0` if unconstrained.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate transaction fields without accessing ledger state.
*
* Enforces two structural invariants: `sfIssuer` must not be the zero
* account ID, and `sfCredentialType` must be non-empty and within
* `kMAX_CREDENTIAL_TYPE_LENGTH` bytes. Both violations produce `tem`
* codes, meaning the transaction is permanently invalid and will not be
* retried.
*
* @param ctx Preflight context carrying the transaction and rule set.
* @return `tesSUCCESS` on valid input; `temINVALID_ACCOUNT_ID` if
* `sfIssuer` is zero; `temMALFORMED` if `sfCredentialType` is empty
* or too long.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks before applying the transaction.
*
* Verifies that the issuer account exists, that the credential object
* keyed by `(subject, issuer, credentialType)` is present in the ledger,
* and that `lsfAccepted` has not already been set on it.
*
* @param ctx Preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if all checks pass; `tecNO_ISSUER` if the issuer
* account does not exist; `tecNO_ENTRY` if the credential object is
* absent; `tecDUPLICATE` if the credential is already accepted.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the acceptance state transition against the mutable ledger view.
*
* Checks that the subject has sufficient reserve to take ownership of the
* credential (comparing `preFeeBalance_` against the post-increment owner
* reserve). If the credential has expired by `parentCloseTime`, deletes it
* via `credentials::deleteSLE` and returns `tecEXPIRED`. On the happy
* path, sets `lsfAccepted` on the credential SLE and transfers the owner
* count: issuer decremented by 1, subject incremented by 1.
*
* @return `tesSUCCESS` on successful acceptance; `tecINSUFFICIENT_RESERVE`
* if the subject cannot afford the incremented reserve; `tecEXPIRED`
* if the credential has passed its expiration (credential is deleted);
* `tefINTERNAL` if account SLEs or the credential SLE cannot be loaded
* (unreachable under correct ledger invariants).
* @note The reserve is checked against `preFeeBalance_` (balance before
* fee deduction), ensuring the fee has already been accounted for.
*/
TER
doApply() override;
/** Invariant visitor hook; no credential-specific invariants are checked. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Invariant finalizer hook; no credential-specific invariants are checked.
*
* @return Always `true` (no invariants to enforce yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,33 +4,117 @@
namespace xrpl {
/** Transactor for the CredentialCreate transaction type.
*
* Implements the issuance step of the W3C Verifiable Credentials (VC)
* lifecycle on the XRP Ledger. A trusted issuer attests facts about a subject
* account by submitting this transaction; the resulting credential SLE can be
* inspected by third parties without contacting the issuer again.
*
* The issuer bears the reserve cost for the credential until the subject
* accepts it via `CredentialAccept`. For self-issued credentials (subject ==
* issuer) the credential is immediately marked `lsfAccepted` and inserted into
* only the single shared owner directory.
*
* @see CredentialAccept, CredentialDelete
*/
class CredentialCreate : public Transactor
{
public:
/** Standard consequence semantics: consumes a sequence number, no blocking
* or custom consequence logic.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Construct a CredentialCreate transactor for the given apply context. */
explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Return the set of flags permitted on this transaction type.
*
* When `fixInvalidTxFlags` is active, restricts accepted flags to
* `tfUniversalMask`, causing any unknown flag bits to be rejected as
* malformed. Returns `0` (allow any flags) on pre-fix ledgers to preserve
* backward compatibility.
*
* @param ctx Preflight context carrying the current rule set.
* @return Bitmask of valid non-universal flags, or `0` if unconstrained.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate transaction fields without accessing ledger state.
*
* Checks three structural invariants: `sfSubject` must be present and
* non-zero; the optional `sfURI`, if provided, must be non-empty and
* within `kMAX_CREDENTIAL_URI_LENGTH` bytes; and `sfCredentialType` must
* be non-empty and within `kMAX_CREDENTIAL_TYPE_LENGTH` bytes. All
* violations produce `temMALFORMED`, permanently rejecting the
* transaction without charging a fee.
*
* @param ctx Preflight context carrying the transaction and rule set.
* @return `tesSUCCESS` on valid input; `temMALFORMED` if `sfSubject` is
* absent, `sfURI` is empty or too long, or `sfCredentialType` is empty
* or too long.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks before applying the transaction.
*
* Verifies that the target subject account exists in the ledger and that
* no credential keyed by `(subject, issuer, credentialType)` already
* exists. These checks require ledger lookups and are therefore separated
* from `preflight`, which is cacheable across ledger closes.
*
* @param ctx Preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if all checks pass; `tecNO_TARGET` if the subject
* account does not exist; `tecDUPLICATE` if the credential triple is
* already present in the ledger.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Create the credential SLE and insert it into the ledger.
*
* Performs three sequential checks before mutating ledger state:
* (1) if `sfExpiration` is present and already behind the ledger's
* `parentCloseTime`, returns `tecEXPIRED` without creating the object;
* (2) confirms the issuer holds sufficient reserve for one additional owned
* object, comparing against `preFeeBalance_`; (3) inserts the credential
* into the issuer's owner directory and increments the issuer's owner count.
*
* For third-party credentials (subject != issuer) the object is also
* inserted into the subject's owner directory (without incrementing the
* subject's owner count), enabling `CredentialAccept` to later transfer
* reserve ownership. For self-issued credentials the `lsfAccepted` flag is
* set immediately and only the single shared directory is used.
*
* @return `tesSUCCESS` on successful creation; `tecEXPIRED` if the
* supplied expiration is already in the past; `tecINSUFFICIENT_RESERVE`
* if the issuer cannot afford the incremented reserve; `tecDIR_FULL`
* if either owner directory has no space for a new entry;
* `tefINTERNAL` if the issuer's `AccountRoot` SLE cannot be loaded
* (unreachable under correct ledger invariants).
* @note Expiration is compared against `parentCloseTime`, not the current
* ledger's own close time, consistent with XRPL's convention for
* time-sensitive operations.
*/
TER
doApply() override;
/** Invariant visitor hook; no credential-specific invariants are checked. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Invariant finalizer hook; no credential-specific invariants are checked.
*
* @return Always `true` (no invariants to enforce yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,33 +4,105 @@
namespace xrpl {
/** Transactor for the CredentialDelete transaction type.
*
* Removes a `Credential` ledger object — the on-chain record that an issuer
* has asserted a verifiable claim about a subject — from the ledger state.
* Either the subject or the issuer may delete a credential unconditionally.
* A third party may only delete a credential that has already expired, which
* allows orphaned objects to be pruned by anyone and their reserve reclaimed.
*
* @see CredentialCreate, CredentialAccept
*/
class CredentialDelete : public Transactor
{
public:
/** Standard consequence semantics: consumes a sequence number, no blocking
* or custom consequence logic.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Construct a CredentialDelete transactor for the given apply context. */
explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Return the set of flags permitted on this transaction type.
*
* When `fixInvalidTxFlags` is active, restricts accepted flags to
* `tfUniversalMask`, causing any unknown flag bits to be rejected as
* malformed. Returns `0` (allow any flags) on pre-fix ledgers to preserve
* backward compatibility.
*
* @param ctx Preflight context carrying the current rule set.
* @return Bitmask of valid non-universal flags, or `0` if unconstrained.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate transaction fields without accessing ledger state.
*
* Enforces three structural invariants: at least one of `sfSubject` or
* `sfIssuer` must be present (both absent → `temMALFORMED`, as the
* credential cannot be identified); any present `sfSubject` or `sfIssuer`
* must be a non-zero `AccountID`; and `sfCredentialType` must be non-empty
* and within `kMAX_CREDENTIAL_TYPE_LENGTH` bytes.
*
* @param ctx Preflight context carrying the transaction and rule set.
* @return `tesSUCCESS` on valid input; `temMALFORMED` if both identity
* fields are absent or `sfCredentialType` is empty or too long;
* `temINVALID_ACCOUNT_ID` if `sfSubject` or `sfIssuer` is the zero
* account.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks before applying the transaction.
*
* Derives the credential keylet from `(subject, issuer, credentialType)`,
* substituting `sfAccount` for any absent `sfSubject` or `sfIssuer`. This
* default-to-sender substitution is how self-deletion works: a subject can
* omit `sfIssuer` when they are also the sender, and vice-versa for the
* issuer. Returns `tecNO_ENTRY` if no matching credential exists.
*
* @param ctx Preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if the credential exists; `tecNO_ENTRY` otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Remove the credential SLE from the ledger.
*
* Applies the same sender-as-default substitution for absent `sfSubject`
* or `sfIssuer` as `preclaim`. Authorization is then checked: if the
* sender is neither the subject nor the issuer, deletion is only permitted
* when the credential has expired (as determined by comparing `sfExpiration`
* against `parentCloseTime`). A valid, unexpired credential may not be
* deleted by a third party. Authorized deletions are delegated to
* `credentials::deleteSLE()`, which unlinks the object from the owner
* directory and decrements the owner's reserve count.
*
* @return `tesSUCCESS` on successful deletion; `tecNO_PERMISSION` if the
* sender is a third party and the credential has not yet expired;
* `tefINTERNAL` if the SLE cannot be loaded after passing `preclaim`
* (indicates ledger corruption, unreachable in practice).
* @note Expiration is evaluated against `parentCloseTime`, consistent with
* XRPL's convention for time-sensitive ledger operations.
*/
TER
doApply() override;
/** Invariant visitor hook; no credential-specific invariants are checked. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Invariant finalizer hook; no credential-specific invariants are checked.
*
* @return Always `true` (no invariants to enforce yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,30 +4,95 @@
namespace xrpl {
/** Transactor for the `DelegateSet` transaction type.
*
* Creates, updates, or deletes a `Delegate` ledger entry that grants a
* delegate account a named set of transaction permissions on behalf of the
* grantor. A single transaction covers all three operations: the action is
* determined by whether `sfPermissions` is non-empty and whether an existing
* `Delegate` SLE is present in the ledger.
*
* @note Self-delegation (grantor == delegate) is rejected in `preflight`.
* An empty `sfPermissions` array with no existing delegate object is
* rejected in `preclaim` to prevent a fee-charging no-op.
*/
class DelegateSet : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
/** Construct a `DelegateSet` transactor bound to the given apply context.
*
* @param ctx The apply context for this transaction.
*/
explicit DelegateSet(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Validate the transaction structure without ledger access.
*
* Rejects if `sfPermissions` exceeds `kPERMISSION_MAX_SIZE`, if the
* grantor and delegate are the same account, if any permission value
* appears more than once, or if any permission value is not delegable
* under the current rules.
*
* @param ctx The preflight context carrying the transaction and rules.
* @return `tesSUCCESS` on success; a `tem*` code on structural failure.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate ledger preconditions against a read-only view.
*
* Confirms that both the grantor account and the `sfAuthorize` target
* account exist in the ledger. Rejects with `tecNO_ENTRY` if an empty
* permissions array is submitted but no existing `Delegate` SLE is found
* (delete-intent with no object to delete).
*
* @param ctx The preclaim context carrying the transaction and a
* read-only ledger view.
* @return `tesSUCCESS`, `terNO_ACCOUNT`, `tecNO_TARGET`, or `tecNO_ENTRY`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the transaction to the mutable ledger view.
*
* Dispatches across three paths based on current ledger state:
* - **Update**: existing `Delegate` SLE + non-empty permissions →
* replaces `sfPermissions` in place, no reserve change.
* - **Delete**: existing `Delegate` SLE + empty permissions →
* delegates to `deleteDelegate()`.
* - **Create**: no existing SLE + non-empty permissions → checks
* reserve, allocates a new `Delegate` SLE, inserts it into both the
* grantor's and the delegate's owner directories, and increments the
* grantor's owner count.
*
* @return `tesSUCCESS` on success; `tecINSUFFICIENT_RESERVE` if the
* grantor cannot cover the reserve for a new object; `tecDIR_FULL`
* if an owner directory is full; `tecINTERNAL` if the no-SLE /
* empty-permissions branch is reached (defensive guard — `preclaim`
* should have blocked this case).
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry
*
* No transaction-specific invariants are enforced; this is a no-op
* reserved for future use.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants
*
* No transaction-specific invariants are enforced; always returns
* `true`. Reserved for future use.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -36,7 +101,23 @@ public:
ReadView const& view,
beast::Journal const& j) override;
// Interface used by AccountDelete
/** Remove a `Delegate` SLE from the ledger and clean up its directory
* entries.
*
* Called by `AccountDelete` to clean up delegate objects owned by an
* account being deleted, and by `doApply()` when the submitter passes
* an empty `sfPermissions` array. Removes the SLE from both the
* grantor's owner directory (via `sfOwnerNode`) and, if present, the
* delegate's owner directory (via `sfDestinationNode`), decrements the
* grantor's owner count, then erases the SLE.
*
* @param view The mutable ledger view to apply the deletion to.
* @param sle The `Delegate` SLE to remove; must not be null.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on success; `tefBAD_LEDGER` if a directory
* removal fails (indicates ledger state corruption, logged at
* `fatal` severity and marked unreachable in coverage).
*/
static TER
deleteDelegate(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j);
};

View File

@@ -4,71 +4,121 @@
namespace xrpl {
/** AMMBid implements AMM bid Transactor.
* This is a mechanism for an AMM instance to auction-off
* the trading advantages to users (arbitrageurs) at a discounted
* TradingFee for a 24 hour slot. Any account that owns corresponding
* LPTokens can bid for the auction slot of that AMM instance.
* Part of the proceeds from the auction, i.e. LPTokens are refunded
* to the current slot-holder computed on a pro rata basis.
* Remaining part of the proceeds - in the units of LPTokens- is burnt,
* thus effectively increasing the LPs shares.
* Total slot time of 24 hours is divided into 20 equal intervals.
* The auction slot can be in any of the following states at any time:
* - Empty - no account currently holds the slot.
* - Occupied - an account owns the slot with at least 5% of the remaining
* slot time (in one of 1-19 intervals).
* - Tailing - an account owns the slot with less than 5% of the remaining time.
* The slot-holder owns the slot privileges when in state Occupied or Tailing.
* If x is the fraction of used slot time for the current slot holder
* and X is the price at which the slot can be bought specified in LPTokens
* then: The minimum bid price for the slot in first interval is
* f(x) = X * 1.05 + min_slot_price
* The bid price of slot any time is
* f(x) = X * 1.05 * (1 - x^60) + min_slot_price, where min_slot_price
* is a constant fraction of the total LPTokens.
* The revenue from a successful bid is split between the current slot-holder
* and the pool. The current slot holder is always refunded the remaining slot
* value f(x) = (1 - x) * X.
* The remaining LPTokens are burnt.
* The auction information is maintained in AuctionSlot of ltAMM object.
* AuctionSlot contains:
* Account - account id, which owns the slot.
* Expiration - slot expiration time
* DiscountedFee - trading fee charged to the account, default is 0.
* Price - price paid for the slot in LPTokens.
* AuthAccounts - up to four accounts authorized to trade at
* the discounted fee.
* @see [XLS30d:Continuous Auction
* Mechanism](https://github.com/XRPLF/XRPL-Standards/discussions/78)
/** Transactor for the AMM continuous auction slot bid (XLS-30d).
*
* Any LP holding tokens for an AMM pool may bid LP tokens for a 24-hour
* trading-fee discount slot, subdivided into 20 equal intervals (~72 min
* each). Auction slot states:
* - **Empty** — no current holder; minimum price applies.
* - **Occupied** — holder is in intervals 018; outbid formula applies.
* - **Tailing** — holder is in the final interval; holder pays minimum price
* only and receives no refund, discouraging last-minute camping.
*
* Pricing curve (X = price paid by current holder, x = fraction of time used):
* @code
* computedPrice = X * 1.05 * (1 - x^60) + minSlotPrice
* @endcode
* The `x^60` term decays rapidly early in the slot and flattens near expiry.
* The 5 % multiplier prevents token-free squatting.
*
* Bid revenue is split: `(1 - x) * X` LP tokens are refunded to the
* outgoing holder; the remainder is burned, increasing the proportional
* share of all remaining LPs.
*
* All ledger mutations are applied in a `Sandbox` and committed atomically
* only on success.
*
* @see https://github.com/XRPLF/XRPL-Standards/discussions/78
*/
class AMMBid : public Transactor
{
public:
/** Normal consequences: bid consumes a bounded LP token amount and does not
* block other transactions from the same account while queued. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit AMMBid(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Gate the transaction on the AMM and MPT amendments.
*
* Returns false (yielding `temDISABLED`) if the base AMM feature is not
* active, or if either pool asset is an MPT and `featureMPTokensV2` is not
* yet enabled.
*
* @param ctx Preflight context providing the active ledger rules.
* @return true if the transaction type is enabled; false otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Stateless validation of the bid transaction fields.
*
* Rejects malformed asset pairs, validates that `sfBidMin` and `sfBidMax`
* are well-formed LP token amounts when present, and enforces that
* `sfAuthAccounts` contains at most `kAUCTION_SLOT_MAX_AUTH_ACCOUNTS`
* entries. Under `fixAMMv1_3`, additionally rejects duplicate accounts and
* self-authorization in `sfAuthAccounts`.
*
* @param ctx Preflight context (no ledger access).
* @return `tesSUCCESS` or a `tem*` code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation for the bid transaction.
*
* Verifies the AMM object exists for the specified asset pair, the pool is
* not empty (`sfLPTokenBalance != 0`), all `sfAuthAccounts` entries
* reference existing on-ledger accounts, the submitter holds LP tokens, and
* any `sfBidMin`/`sfBidMax` amounts use the correct LP token asset and do
* not exceed the submitter's balance or the total pool balance.
*
* @param ctx Preclaim context with read-only ledger view.
* @return `tesSUCCESS`, a `ter*` retryable code, or a fee-claiming `tec*`
* code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the auction slot bid against a `Sandbox` view.
*
* Delegates to the file-local `applyBid` helper, which computes the bid
* price, refunds the outgoing slot holder (if any), burns the remainder of
* the payment as LP tokens, and writes the updated `sfAuctionSlot` object
* inside `ltAMM`. The sandbox is committed to the real ledger view only on
* full success; any failure leaves the ledger untouched.
*
* @return `tesSUCCESS` on success, or `tecAMM_FAILED` /
* `tecAMM_INVALID_TOKENS` / `tecINTERNAL` on failure.
*/
TER
doApply() override;
/** No-op stub; no transaction-specific invariant entries for `AMMBid` yet.
*
* @param isDelete True if the entry was erased.
* @param before Entry state before the transaction.
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op stub; no transaction-specific post-conditions for `AMMBid` yet.
*
* Always returns true. Reserved for future transaction-specific invariants.
*
* @param tx The transaction being applied.
* @param result The tentative TER result.
* @param fee Fee consumed by the transaction.
* @param view Read-only ledger view after the transaction.
* @param j Journal for logging.
* @return true unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,9 +1,35 @@
/** @file
* Declares the `AMMClawback` transactor (XLS-73), which allows a regulated
* token issuer to recover assets held inside an AMM liquidity position.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class Sandbox;
/** Transactor for the `AMMClawback` transaction type (XLS-73).
*
* Allows a regulated-token issuer to reclaim assets from a specific holder's
* AMM liquidity position. This prevents holders from circumventing issuer
* clawback authority by depositing regulated tokens into an AMM pool.
*
* The clawback is implemented as a forced proportional withdrawal of the
* holder's LP tokens via `AMMWithdraw`'s helpers, with the proceeds sent to
* the issuer. Two modes are supported: full clawback (all LP tokens) when no
* `sfAmount` is specified, and partial clawback (up to a requested asset1
* quantity) when `sfAmount` is present. Both modes pass a zero trading fee
* because the withdrawal is a proportional equal-ratio removal — charging a
* fee would penalise the issuer for exercising a regulatory right.
*
* All ledger mutations are accumulated in a `Sandbox` inside `doApply` and
* committed only on success, consistent with the standard transactor pattern.
*
* @see AMMWithdraw::equalWithdrawTokens
* @see AMMWithdraw::withdraw
*/
class AMMClawback : public Transactor
{
public:
@@ -13,27 +39,100 @@ public:
{
}
/** Gate the transaction on the `featureAMMClawback` amendment.
*
* Also requires `featureMPTokensV2` when any of `sfAsset`, `sfAsset2`,
* or `sfAmount` refers to an MPT issuance, ensuring MPT support is gated
* behind its own separate amendment rollout.
*
* @param ctx Preflight context providing access to the transaction and
* active amendment rules.
* @return `true` if the required amendments are enabled; `false` otherwise
* (transaction is rejected before preflight runs).
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the set of valid transaction flags for `AMMClawback`.
*
* @param ctx Preflight context (unused; present for interface uniformity).
* @return Bitmask of flags accepted by this transaction type
* (`tfAMMClawbackMask`).
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless validation of the `AMMClawback` transaction fields.
*
* Enforces the following invariants without accessing ledger state:
* - `sfAsset` must not be XRP (only issued assets support clawback).
* - `sfAccount` (issuer) must equal `sfAsset`'s issuer field.
* - `sfHolder` must differ from `sfAccount`.
* - If `tfClawTwoAssets` is set, both `sfAsset` and `sfAsset2` must share
* the same issuer.
* - If `sfAmount` is present, its asset subfield must match `sfAsset` and
* the quantity must be positive.
*
* @param ctx Preflight context providing the transaction and active rules.
* @return `tesSUCCESS` on valid input; `temMALFORMED`, `temINVALID_FLAG`,
* or `temBAD_AMOUNT` on constraint violations.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Ledger-state permission checks for `AMMClawback`.
*
* Verifies that both the issuer account and the target AMM pool exist, and
* that the issuer has clawback authority over the relevant asset(s):
* - For IOU assets: the issuer's `AccountRoot` must have
* `lsfAllowTrustLineClawback` set and must not have `lsfNoFreeze`
* set (permanent freeze-waiver revokes clawback ability).
* - For MPT assets: the specific MPT issuance must carry
* `lsfMPTCanClawback` and must be owned by the transaction's account.
*
* When `tfClawTwoAssets` is set, permission is checked for both assets.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all checks pass; `terNO_ACCOUNT` if issuer or
* holder account is absent; `terNO_AMM` if the pool does not exist;
* `tecNO_PERMISSION` if clawback authority is not established.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the `AMMClawback` transaction against the ledger.
*
* Wraps `applyGuts` in a `Sandbox`. All ledger mutations are accumulated
* in the sandbox and flushed to the real `ApplyView` only if `applyGuts`
* returns `tesSUCCESS`. Failed executions leave no trace beyond fee
* deduction.
*
* @return `tesSUCCESS` on success; a `tec*` code on failure.
*/
TER
doApply() override;
/** Per-SLE invariant visitor — no transaction-specific invariants yet.
*
* @param isDelete `true` if the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant finalizer — no transaction-specific checks yet.
*
* @param tx The applied transaction.
* @param result TER result of the transaction.
* @param fee XRP fee charged.
* @param view Read-only view of the ledger after the transaction.
* @param j Journal for diagnostic logging.
* @return Always `true`; reserved for future invariant checks.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -43,19 +142,52 @@ public:
beast::Journal const& j) override;
private:
/** Core clawback logic executed inside a `Sandbox`.
*
* Reads pool balances, selects full or partial withdrawal based on
* whether `sfAmount` is present, drives `AMMWithdraw` helpers to burn
* LP tokens, and calls `directSendNoFee` to transfer recovered assets
* from the holder to the issuer. Both code paths pass
* `FreezeHandling::IgnoreFreeze` and `AuthHandling::IgnoreAuth` so the
* issuer is not blocked by trustline restrictions they may have set.
*
* @param view Sandbox accumulating all mutations for this transaction.
* @return `tesSUCCESS` on success; a `tec*` error code on failure.
*/
TER
applyGuts(Sandbox& view);
/** Withdraw both assets by providing maximum amount of asset1,
* asset2's amount will be calculated according to the current proportion.
* Since it is two-asset withdrawal, tfee is omitted.
* @param view
* @param ammAccount current AMM account
* @param amountBalance current AMM asset1 balance
* @param amount2Balance current AMM asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount asset1 withdraw amount
* @return
/** Proportional two-asset withdrawal bounded by a maximum asset1 amount.
*
* Computes the pool fraction corresponding to `amount` of asset1, derives
* the matching asset2 and LP-token quantities, then calls
* `AMMWithdraw::withdraw`. No trading fee is charged because both assets
* are withdrawn in the current pool ratio.
*
* If the derived LP-token quantity exceeds `holdLPtokens` (the holder
* does not have enough LP tokens to satisfy the requested asset1 amount),
* the method degrades gracefully to a full clawback of all LP tokens the
* holder owns via `AMMWithdraw::equalWithdrawTokens` — the issuer cannot
* over-claw.
*
* When `fixAMMClawbackRounding` is active, `getRoundedLPTokens` and
* `getRoundedAsset` are applied before calling `withdraw` to snap values
* to representable amounts and prevent dust or rounding-driven invariant
* violations.
*
* @param view Sandbox accumulating ledger mutations.
* @param ammSle The AMM ledger entry.
* @param holder Account whose LP tokens are being redeemed.
* @param ammAccount AMM pseudo-account holding the pooled assets.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP-token supply for the pool.
* @param holdLPtokens LP tokens currently held by `holder`.
* @param amount Maximum asset1 quantity the issuer wishes to recover.
* @return Tuple of `(TER, newLPTokenBalance, asset1Withdrawn,
* asset2Withdrawn)`. `asset2Withdrawn` is always populated for
* equal-ratio withdrawals; it is `std::nullopt` only on internal
* error paths.
*/
std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
equalWithdrawMatchingOneAmount(

View File

@@ -1,3 +1,11 @@
/** @file
* State governor for AMM participation in a single `flow()` call.
*
* `AMMContext` is threaded by reference through `toStrands()`, `flowOne()`,
* and every `AMMLiquidity` instance so that all parts of the payment engine
* share one authoritative counter for AMM iteration bookkeeping.
*/
#pragma once
#include <xrpl/protocol/AccountID.h>
@@ -6,31 +14,56 @@
namespace xrpl {
/** Maintains AMM info per overall payment engine execution and
* individual iteration.
* Only one instance of this class is created in Flow.cpp::flow().
* The reference is percolated through calls to AMMLiquidity class,
* which handles AMM offer generation.
/** Tracks AMM offer consumption state across one payment engine execution.
*
* A single instance is created at the top of `Flow.cpp::flow()` and passed
* by reference into every `AMMLiquidity` object for the life of that call.
* It governs two things:
*
* 1. **Iteration budget** — AMM pools are continuous and can produce a new
* offer at the current spot price after every consumption, unlike CLOB
* offers which are removed after use. `AMMContext` enforces
* `kMAX_ITERATIONS` to bound execution time.
*
* 2. **Offer-sizing strategy** — `multiPath_` tells `AMMLiquidity` whether
* to use quality-matched single-path sizing or Fibonacci-scaled multi-path
* sizing. It is updated dynamically as the set of active strands changes.
*
* @note Non-copyable by design: all `AMMLiquidity` objects hold a reference
* to the shared instance. Copying would create divergent counters that
* the engine could not reconcile.
*
* @see AMMLiquidity, AMMOffer, StrandFlow.h
*/
class AMMContext
{
public:
// Restrict number of AMM offers. If this restriction is removed
// then need to restrict in some other way because AMM offers are
// not counted in the BookStep offer counter.
/** Maximum number of payment engine iterations that may consume an AMM
* offer in a single `flow()` call.
*
* AMM pools are continuous liquidity sources that never become exhausted
* the way CLOB offers do, so `BookStep`'s built-in offer counter does not
* bound them. This constant caps the AMM-specific iteration count to
* prevent unbounded execution. `AMMLiquidity::generateFibSeqOffer` uses
* a Fibonacci table with exactly this many entries.
*/
constexpr static std::uint8_t kMAX_ITERATIONS = 30;
private:
// Tx account owner is required to get the AMM trading fee in BookStep
AccountID account_;
// true if payment has multiple paths
bool multiPath_{false};
// Is true if AMM offer is consumed during a payment engine iteration.
bool ammUsed_{false};
// Counter of payment engine iterations with consumed AMM
std::uint16_t ammIters_{0};
public:
/** Construct with the initiating account and initial path multiplicity.
*
* @param account `AccountID` of the transaction sender. `BookStep`
* uses this to look up the sender's per-account AMM trading fee.
* @param multiPath `true` if the payment already has more than one
* strand at construction time; `false` otherwise. May be updated
* later via `setMultiPath()`.
*/
AMMContext(AccountID const& account, bool multiPath) : account_(account), multiPath_(multiPath)
{
}
@@ -39,24 +72,55 @@ public:
AMMContext&
operator=(AMMContext const&) = delete;
/** Return whether the payment currently has more than one active strand.
*
* When `true`, `AMMLiquidity` sizes synthetic AMM offers using the
* Fibonacci sequence keyed to `curIters()`. When `false`, it sizes them
* so the post-swap spot price matches the best competing CLOB quality.
*
* @return `true` if multi-path mode is active.
*/
[[nodiscard]] bool
multiPath() const
{
return multiPath_;
}
/** Update the path-multiplicity flag.
*
* Called by `StrandFlow.h` after each `activateNext()` because the
* number of active strands can change mid-payment.
*
* @param fs `true` if more than one strand is currently active.
*/
void
setMultiPath(bool fs)
{
multiPath_ = fs;
}
/** Mark that an AMM offer was consumed during the current iteration.
*
* Called from `AMMOffer::consume()` the moment an AMM offer is accepted.
* The flag is read by `update()` to decide whether to increment the
* iteration counter, then cleared unconditionally.
*/
void
setAMMUsed()
{
ammUsed_ = true;
}
/** Commit the current iteration's AMM usage to the running counter.
*
* Increments `ammIters_` if an AMM offer was consumed this iteration
* (i.e., `setAMMUsed()` was called), then resets the per-iteration flag.
* Called once per outer loop iteration in `StrandFlow.h` after the
* winning strand's sandbox has been applied to the main ledger view.
*
* @note Iterations that route entirely through CLOB offers leave
* `ammIters_` unchanged.
*/
void
update()
{
@@ -65,26 +129,49 @@ public:
ammUsed_ = false;
}
/** Return whether the AMM iteration budget has been exhausted.
*
* Checked by `AMMLiquidity::getOffer()` before generating each new AMM
* offer. Once `true`, no further AMM liquidity is offered for this
* payment.
*
* @return `true` if `ammIters_` has reached `kMAX_ITERATIONS`.
*/
[[nodiscard]] bool
maxItersReached() const
{
return ammIters_ >= kMAX_ITERATIONS;
}
/** Return the number of iterations that have consumed AMM liquidity.
*
* Used by `AMMLiquidity::generateFibSeqOffer()` as the index into the
* Fibonacci scaling table. Zero on the first AMM-consuming iteration.
*
* @return Current AMM iteration count, in `[0, kMAX_ITERATIONS)`.
*/
[[nodiscard]] std::uint16_t
curIters() const
{
return ammIters_;
}
/** Return the `AccountID` of the transaction sender.
*
* @return The account used to look up the per-sender AMM trading fee.
*/
[[nodiscard]] AccountID
account() const
{
return account_;
}
/** Strand execution may fail. Reset the flag at the start
* of each payment engine iteration.
/** Reset the per-iteration AMM-used flag before a new strand attempt.
*
* Strand execution can fail and its sandbox is discarded. Resetting
* `ammUsed_` here prevents a failed strand's AMM consumption from being
* double-counted when `update()` is called for the strand that succeeds.
* Called by `StrandFlow.h` at the start of each strand attempt.
*/
void
clear()

View File

@@ -4,35 +4,27 @@
namespace xrpl {
/** AMMCreate implements Automatic Market Maker(AMM) creation Transactor.
* It creates a new AMM instance with two tokens. Any trader, or Liquidity
* Provider (LP), can create the AMM instance and receive in return shares
* of the AMM pool in the form of LPTokens. The number of tokens that LP gets
* are determined by LPTokens = sqrt(A * B), where A and B is the current
* composition of the AMM pool. LP can add (AMMDeposit) or withdraw
* (AMMWithdraw) tokens from AMM and
* AMM can be used transparently in the payment or offer crossing transactions.
* Trading fee is charged to the traders for the trades executed against
* AMM instance. The fee is added to the AMM pool and distributed to the LPs
* in proportion to the LPTokens upon liquidity removal. The fee can be voted
* on by LP's (AMMVote). LP's can continuously bid (AMMBid) for the 24 hour
* auction slot, which enables LP's to trade at zero trading fee.
* AMM instance creates AccountRoot object with disabled master key
* for book-keeping of XRP balance if one of the tokens
* is XRP, a trustline for each IOU token, a trustline to keep track
* of LPTokens, and ltAMM ledger object. AccountRoot ID is generated
* internally from the parent's hash. ltAMM's object ID is
* hash{token1.currency, token1.issuer, token2.currency, token2.issuer}, where
* issue1 < issue2. ltAMM object provides mapping from the hash to AccountRoot
* ID and contains: AMMAccount - AMM AccountRoot ID. TradingFee - AMM voted
* TradingFee. VoteSlots - Array of VoteEntry, contains fee vote information.
* AuctionSlot - Auction slot, contains discounted fee bid information.
* LPTokenBalance - LPTokens outstanding balance.
* AMMToken - currency/issuer information for AMM tokens.
* AMMDeposit, AMMWithdraw, AMMVote, and AMMBid transactions use the hash
* to access AMM instance.
* @see [XLS30d:Creating AMM instance on
* XRPL](https://github.com/XRPLF/XRPL-Standards/discussions/78)
/** Bootstraps a new Automatic Market Maker pool on the XRP Ledger.
*
* `AMMCreate` is the entry point for the AMM DEX subsystem. It creates the
* four categories of ledger objects that constitute an AMM pool: a
* pseudo-account `AccountRoot` (keyed from the AMM keylet, carrying a
* disabled master key and tagged with `sfAMMID`), an `ltAMM` object keyed
* by `hash{asset1.currency, asset1.issuer, asset2.currency, asset2.issuer}`
* in canonical (`std::minmax`) order, the initial LP tokens computed as
* `sqrt(sfAmount * sfAmount2)` and sent to the creator, and the asset
* trustlines/MPToken entries required to hold the seeded liquidity.
*
* No other AMM transaction (`AMMDeposit`, `AMMWithdraw`, `AMMVote`,
* `AMMBid`, `AMMDelete`) can operate until this transaction succeeds.
*
* @note The fee charged is one owner reserve (not the standard base fee)
* because the transaction permanently allocates a scarce ledger object.
* @note LP-token trustlines are created with a zero credit limit. A holder
* can only receive LP tokens through affirmative action (deposit,
* `TrustSet`, offer crossing) — the AMM cannot push tokens to an
* unwilling account.
* @see [XLS-30d: AMM on XRPL](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
class AMMCreate : public Transactor
{
@@ -43,28 +35,96 @@ public:
{
}
/** Gate the transaction on required feature flags.
*
* Returns false (and thereby rejects the transaction before any field
* validation) when the AMM subsystem is not yet active on the current
* rule set, or when either pool asset is an MPT issue and
* `featureMPTokensV2` has not been enabled.
*
* @param ctx The preflight context providing the current rules and tx.
* @return true if all required amendments are active; false otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Validate the transaction fields without ledger access.
*
* Rejects the transaction if the two pool assets are identical
* (`temBAD_AMM_TOKENS`), if either amount is structurally invalid
* (`invalidAMMAmount`), or if `sfTradingFee` exceeds
* `kTRADING_FEE_THRESHOLD` (`temBAD_FEE`).
*
* @param ctx The preflight context containing the transaction.
* @return `tesSUCCESS` on success, or a `tem*` error code on failure.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Return the fee required to submit this transaction.
*
* Overrides the default base-fee logic and returns
* `calculateOwnerReserveFee` instead — one owner reserve increment —
* because `AMMCreate` permanently allocates a ledger object.
*
* @param view Read-only ledger view providing the current fee schedule.
* @param tx The transaction being evaluated.
* @return Fee in drops equal to one owner reserve increment.
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Check ledger state before applying the transaction.
*
* Verifies that no AMM already exists for the asset pair
* (`tecDUPLICATE`), that the creator is authorized for both assets,
* that neither asset is frozen (`tecFROZEN`), that IOU issuers have
* `lsfDefaultRipple` set (`terNO_RIPPLE`), that the creator holds
* sufficient XRP for the LP-token trustline reserve plus seed funds
* (`tecINSUF_RESERVE_LINE`, `tecUNFUNDED_AMM`), and that neither
* seed asset is itself an existing LP token (`tecAMM_INVALID_TOKENS`).
* Also validates MPT permissions via `checkMPTTxAllowed`.
*
* When `featureAMMClawback` is not yet active, assets whose issuers
* have clawback enabled (`lsfAllowTrustLineClawback` or
* `lsfMPTCanClawback`) are rejected with `tecNO_PERMISSION`.
*
* @param ctx The preclaim context providing read-only ledger access.
* @return `tesSUCCESS` on success, or an appropriate error `TER`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Attempt to create the AMM instance. */
/** Create all AMM ledger objects and seed initial liquidity.
*
* Executes inside a `Sandbox` overlay: the pseudo-account, `ltAMM`
* object, LP tokens, and asset trustlines/MPToken entries are created
* inside `applyCreate()`. The sandbox is flushed to the live view only
* when `applyCreate()` returns `tesSUCCESS`; any earlier failure leaves
* the ledger unchanged. On success, both directions of the trading pair
* are registered in the `OrderBookDB`.
*
* @return `tesSUCCESS` on success, or a `tec*` error code on failure.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry
*
* Currently a no-op for `AMMCreate`; reserved for future
* transaction-specific invariants.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants
*
* Currently returns true unconditionally for `AMMCreate`; reserved for
* future transaction-specific post-conditions.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,11 +4,24 @@
namespace xrpl {
/** AMMDelete implements AMM delete transactor. This is a mechanism to
* delete AMM in an empty state when the number of LP tokens is 0.
* AMMDelete deletes the trustlines up to configured maximum. If all
* trustlines are deleted then AMM ltAMM and root account are deleted.
* Otherwise AMMDelete should be called again.
/** Removes a fully-drained AMM pool and its associated ledger objects.
*
* When all liquidity has been withdrawn via `AMMWithdraw` and
* `sfLPTokenBalance` on the `ltAMM` entry reaches zero, the pool's
* supporting objects (trustlines, MPToken entries, the `ltAMM` record, and
* the AMM pseudo-account `AccountRoot`) are not cleaned up automatically.
* `AMMDelete` handles that deferred cleanup.
*
* Because a long-lived pool may accumulate hundreds of LP-token trustlines,
* deletion is chunked: each invocation removes at most
* `maxDeletableAMMTrustLines` (512) trustlines and commits that partial
* progress to the ledger. If trustlines remain, `doApply` returns
* `tecINCOMPLETE` and the submitter must re-submit until the pool is fully
* removed.
*
* @note Deletion is only permitted when `sfLPTokenBalance` is exactly zero.
* Use `AMMWithdraw` to drain a pool that still holds liquidity.
* @see AMMCreate, AMMWithdraw
*/
class AMMDelete : public Transactor
{
@@ -19,24 +32,79 @@ public:
{
}
/** Gate the transaction on required feature flags.
*
* Returns false when the base AMM feature is not yet active, or when
* either pool asset is an MPT issue and `featureMPTokensV2` has not
* been enabled. The MPT gate prevents deletion of MPT-based pools on
* network versions that do not fully support MPT cleanup.
*
* @param ctx The preflight context providing the current rules and tx.
* @return true if all required amendments are active; false otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Validate the transaction fields without ledger access.
*
* All amendment checks are handled by `checkExtraFeatures`; there are
* no field-level constraints that can be verified without consulting
* ledger state. Always returns `tesSUCCESS`.
*
* @param ctx The preflight context containing the transaction.
* @return `tesSUCCESS` unconditionally.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Verify the AMM pool exists and has been fully drained.
*
* Reads the `ltAMM` object for the submitted asset pair and checks that
* `sfLPTokenBalance` is exactly zero. Returns `terNO_AMM` if no such
* pool exists, or `tecAMM_NOT_EMPTY` if LP tokens are still outstanding.
* Both failure codes prevent fee collection; use `AMMWithdraw` to drain
* a pool before deleting it.
*
* @param ctx The preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the pool exists and is empty; `terNO_AMM` if
* the asset pair has no AMM; `tecAMM_NOT_EMPTY` if LP tokens remain.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Delete AMM trustlines and, when all are gone, the pool itself.
*
* Delegates to `deleteAMMAccount()` inside an isolated `Sandbox` view.
* That helper deletes up to `maxDeletableAMMTrustLines` (512) trustlines
* per call. The sandbox is applied to the live ledger on both
* `tesSUCCESS` and `tecINCOMPLETE`, committing partial progress even
* when the deletion is not yet complete. When `tesSUCCESS` is returned,
* any remaining MPToken entries, the `ltAMM` object, and the AMM
* pseudo-account `AccountRoot` are also erased.
*
* @return `tesSUCCESS` when the pool and all its objects are fully
* removed; `tecINCOMPLETE` when trustlines remain and the caller
* must re-submit.
*/
TER
doApply() override;
/** @copydoc Transactor::visitInvariantEntry
*
* Currently a no-op for `AMMDelete`; reserved for future
* transaction-specific invariants.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** @copydoc Transactor::finalizeInvariants
*
* Currently returns true unconditionally for `AMMDelete`; reserved for
* future transaction-specific post-conditions.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -6,69 +6,143 @@ namespace xrpl {
class Sandbox;
/** AMMDeposit implements AMM deposit Transactor.
* The deposit transaction is used to add liquidity to the AMM instance pool,
* thus obtaining some share of the instance's pools in the form of LPTokens.
* If the trader deposits proportional values of both assets without changing
* their relative price, then no trading fee is charged on the transaction.
* The trader can specify different combination of the fields in the deposit.
* LPTokens - transaction assumes proportional deposit of pools assets in
* exchange for the specified amount of LPTokens of the AMM instance.
* Asset1In - transaction assumes single asset deposit of the amount of asset
* specified by Asset1In. This is essentially a swap and an equal asset
* deposit.
* Asset1In and Asset2In - transaction assumes proportional deposit of pool
* assets with the constraints on the maximum amount of each asset that
* the trader is willing to deposit.
* Asset1In and LPTokens - transaction assumes that a single asset asset1
* is deposited to obtain some share of the AMM instance's pools
* represented by amount of LPTokens.
* Asset1In and EPrice - transaction assumes single asset deposit with
* the following two constraints:
* a. amount of asset1 if specified (not 0) in Asset1In specifies the
* maximum amount of asset1 that the trader is willing to deposit b. The
* effective-price of the LPTokens traded out does not exceed the specified
* EPrice. Following updates after a successful AMMDeposit transaction: The
* deposited asset, if XRP, is transferred from the account that initiated the
* transaction to the AMM instance account, thus changing the Balance field of
* each account. The deposited asset, if tokens, are balanced between the AMM
* account and the issuer account trustline. The LPTokens are issued by the AMM
* instance account to the account that initiated the transaction and a new
* trustline is created, if there does not exist one. The pool composition is
* updated.
* @see [XLS30d:AMMDeposit
* transaction](https://github.com/XRPLF/XRPL-Standards/discussions/78)
/** Transactor for `AMMDeposit` transactions (XLS-30d).
*
* Allows liquidity providers to add assets to an AMM pool in exchange for
* LP tokens that represent their fractional share of the pool's reserves.
* Six mutually exclusive deposit modes are supported, selected by a single
* flag bit from `tfDepositSubTx`:
*
* | Flag | Method | Fee charged? | Description |
* |---------------------|---------------------------|:------------:|-------------|
* | `tfLPToken` | `equalDepositTokens` | No | Proportional deposit targeting a specific LP token amount |
* | `tfTwoAsset` | `equalDepositLimit` | No | Proportional deposit with per-asset maximum constraints |
* | `tfSingleAsset` | `singleDeposit` | Yes | Single-asset deposit by amount |
* | `tfOneAssetLPToken` | `singleDepositTokens` | Yes | Single-asset deposit targeting an LP token quantity |
* | `tfLimitLPToken` | `singleDepositEPrice` | Yes | Single-asset deposit with effective-price ceiling |
* | `tfTwoAssetIfEmpty` | `equalDepositInEmptyState`| N/A | Bootstraps an AMM pool whose LP token balance is zero |
*
* Proportional modes (the first two) do not charge a trading fee because they
* preserve the pool's price ratio. Single-asset modes are mathematically
* equivalent to an implicit swap followed by a proportional deposit, so the
* pool's trading fee applies to the swap component.
*
* On success, XRP deposits adjust account `sfBalance` fields directly; IOU
* and MPT deposits move balances through trustlines between the depositor and
* the AMM account. LP tokens are issued to the depositor, creating a new
* trustline if none exists.
*
* Unlike `AMMWithdraw`, which exposes `withdraw` and `equalWithdrawTokens` as
* `public static` methods for reuse by `AMMDelete`, all deposit-mode helpers
* here are `private` — the deposit path is only reachable through the
* validated transactor lifecycle.
*
* @see [XLS-30d AMMDeposit](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
class AMMDeposit : public Transactor
{
public:
/** Standard fee/sequence consequences; no special queue-blocking behavior. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit AMMDeposit(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Gate on the `featureAMM` amendment and, for MPT-backed pools, on
* `featureMPTokensV2`.
*
* Returns `false` (disabling this transaction type entirely) if `featureAMM`
* is not active. Additionally returns `false` if the pool asset pair or any
* deposit amount uses an `MPTIssue` but `featureMPTokensV2` has not yet
* activated, keeping MPT pool deposits gated behind the second amendment.
*
* @param ctx The preflight context providing rules and the transaction.
* @return `true` if the transaction type is permitted under current rules.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the valid transaction flag mask (`tfAMMDepositMask`).
*
* The framework uses this to reject any transaction whose flags fall
* outside the set of bits defined for `AMMDeposit`.
*
* @param ctx Unused; present for the static dispatch interface.
* @return Bitmask of all valid `AMMDeposit` flags.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate the transaction structure without accessing ledger state.
*
* Enforces:
* - Exactly one flag bit from `tfDepositSubTx` is set
* (`std::popcount == 1`); otherwise `temMALFORMED`.
* - Per-mode field constraints (required and forbidden optional fields).
* - Asset-pair validity via `invalidAMMAssetPair`.
* - `sfAmount` and `sfAmount2` denominate different assets.
* - `sfLPTokenOut`, if present, is positive.
* - `sfTradingFee`, if present, does not exceed `kTRADING_FEE_THRESHOLD`.
*
* @param ctx The preflight context providing the transaction and rules.
* @return `tesSUCCESS` on success, or a `tem*` error code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate against current ledger state (read-only).
*
* Runs after signature verification. Key checks:
* - The AMM ledger entry for the declared asset pair must exist;
* absent → `terNO_AMM`.
* - For `tfTwoAssetIfEmpty`: LP token balance must be zero
* (`tecAMM_NOT_EMPTY` if already populated).
* - For all other modes: LP token balance must be positive
* (`tecAMM_EMPTY` if the pool is drained).
* - Account has sufficient funds for the requested deposit amounts.
* Re-checked inside `deposit()` for modes where the final amount is
* derived from pool math rather than specified directly.
* - When `featureAMMClawback` is active: neither pool asset may be
* individually frozen on the depositor's account (`tecFROZEN`).
* - MPT-specific authorization via `checkMPTTxAllowed`.
*
* @param ctx The preclaim context providing the read-only ledger view.
* @return `tesSUCCESS` on success, or the appropriate `tec*`/`ter*` code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the deposit against a copy-on-write sandbox and commit on success.
*
* Creates a `Sandbox` over `ctx_.view()`, delegates to `applyGuts`, and
* calls `sb.apply(ctx_.rawView())` only if `applyGuts` returns success.
* Any failure leaves the consensus view unmodified.
*
* @return `tesSUCCESS` or the `tec*` error from the selected deposit mode.
*/
TER
doApply() override;
/** No-op placeholder for future transaction-specific invariant state.
*
* `AMMDeposit` currently delegates all invariant checking to the global
* `ValidAMM` invariant checker. This override exists to satisfy the
* `Transactor` interface and is reserved for future per-transaction
* invariants.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op placeholder for future transaction-specific invariant finalization.
*
* Always returns `true`. See `visitInvariantEntry` for rationale.
*
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -78,23 +152,49 @@ public:
beast::Journal const& j) override;
private:
/** Dispatch to the mode-specific deposit helper and commit AMM state.
*
* Reads the current pool balances, determines the effective trading fee
* (pool fee for non-empty pools; `sfTradingFee` from the transaction for
* the empty-pool bootstrap path), then routes to the appropriate private
* method based on `tfDepositSubTx`. On `tesSUCCESS`, updates
* `sfLPTokenBalance` on the AMM ledger entry and, for the empty-pool case,
* calls `initializeFeeAuctionVote` to grant the bootstrapping LP their
* initial fee-governance position.
*
* @param view The sandbox to mutate; committed by the caller only on success.
* @return `{TER, true}` on success; `{tec*, false}` on failure.
*/
std::pair<TER, bool>
applyGuts(Sandbox& view);
/** Deposit requested assets and token amount into LP account.
* Return new total LPToken balance.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amountDeposit
* @param amount2Deposit
* @param lptAMMBalance current AMM LPT balance
* @param lpTokensDeposit amount of tokens to deposit
* @param depositMin minimum accepted amount deposit
* @param deposit2Min minimum accepted amount2 deposit
* @param lpTokensDepositMin minimum accepted LPTokens deposit
* @param tfee trading fee in basis points
* @return
/** Shared execution kernel: transfer assets into the AMM and issue LP tokens.
*
* Adjusts the requested amounts via `adjustAmountsByLPTokens` (rounding
* and token-cap corrections), then verifies minimum constraints and account
* balance. On success, performs three `accountSend` transfers in order:
* depositor → AMM for asset1, depositor → AMM for asset2 (if present),
* and AMM → depositor for the LP tokens (creating a trustline if needed).
*
* This function re-checks account balance even for modes where `preclaim`
* already validated it, because the actual amounts may differ from the
* values specified in the transaction (they are derived from pool math).
*
* @param view Sandbox to mutate.
* @param ammAccount AccountID of the AMM pseudo-account.
* @param amountBalance Current pool balance of asset1.
* @param amountDeposit Computed asset1 amount to transfer.
* @param amount2Deposit Computed asset2 amount to transfer, or absent for
* single-asset modes.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokensDeposit LP tokens to issue to the depositor.
* @param depositMin Minimum acceptable asset1 deposit; `tecAMM_FAILED`
* if the adjusted amount falls below this.
* @param deposit2Min Minimum acceptable asset2 deposit; same semantics.
* @param lpTokensDepositMin Minimum acceptable LP token issuance; same semantics.
* @param tfee Trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` on success, or
* `{tec*, STAmount{}}` on failure.
*/
std::pair<TER, STAmount>
deposit(
@@ -110,18 +210,25 @@ private:
std::optional<STAmount> const& lpTokensDepositMin,
std::uint16_t tfee);
/** Equal asset deposit (LPTokens) for the specified share of
* the AMM instance pools. The trading fee is not charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amount2Balance current AMM asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param lpTokensDeposit amount of tokens to deposit
* @param depositMin minimum accepted amount deposit
* @param deposit2Min minimum accepted amount2 deposit
* @param tfee trading fee in basis points
* @return
/** Proportional two-asset deposit for a targeted LP token amount (`tfLPToken`).
*
* Computes asset1 and asset2 deposit amounts proportional to `lpTokensDeposit`
* relative to the current LP supply (`lptAMMBalance`), then forwards to
* `deposit`. Under `fixAMMv1_3`, if token adjustment rounds to zero the
* transaction fails with `tecAMM_INVALID_TOKENS`. No trading fee is charged
* because the deposit preserves the pool's price ratio.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokensDeposit Target LP token amount to receive.
* @param depositMin Optional minimum asset1 deposit; `tecAMM_FAILED` if not met.
* @param deposit2Min Optional minimum asset2 deposit; same semantics.
* @param tfee Trading fee in basis points (unused for fee charging; passed
* to `deposit` for amount-adjustment rounding).
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
equalDepositTokens(
@@ -135,19 +242,28 @@ private:
std::optional<STAmount> const& deposit2Min,
std::uint16_t tfee);
/** Equal asset deposit (Asset1In, Asset2In) with the constraint on
* the maximum amount of both assets that the trader is willing to deposit.
* The trading fee is not charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amount2Balance current AMM asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount maximum asset1 deposit amount
* @param amount2 maximum asset2 deposit amount
* @param lpTokensDepositMin minimum accepted LPTokens deposit
* @param tfee trading fee in basis points
* @return
/** Proportional two-asset deposit bounded by per-asset maximums (`tfTwoAsset`).
*
* Computes the LP token fraction implied by `amount` (asset1 max), then
* derives the required asset2. If asset2 exceeds `amount2`, re-derives from
* `amount2` and checks that asset1 stays within `amount`. If neither
* binding is satisfiable the transaction fails with `tecAMM_FAILED`.
* No trading fee is charged; the deposit preserves the pool price ratio.
*
* Equations used (A = pool asset1, B = pool asset2, T = LP supply):
* `a = (t/T) * A`, `b = (t/T) * B`, solved for whichever asset is the
* binding constraint.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP token supply.
* @param amount Maximum asset1 the depositor will provide.
* @param amount2 Maximum asset2 the depositor will provide.
* @param lpTokensDepositMin Optional minimum LP tokens to receive; `tecAMM_FAILED` if not met.
* @param tfee Trading fee in basis points (passed through for rounding).
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
equalDepositLimit(
@@ -161,16 +277,22 @@ private:
std::optional<STAmount> const& lpTokensDepositMin,
std::uint16_t tfee);
/** Single asset deposit (Asset1In) by the amount.
* The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount requested asset1 deposit amount
* @param lpTokensDepositMin minimum accepted LPTokens deposit
* @param tfee trading fee in basis points
* @return
/** Single-asset deposit by amount (`tfSingleAsset`).
*
* Computes LP tokens issued via the single-deposit formula
* `t = T * (b/B - x) / (1 + x)` where `x = sqrt(f1² + b/(B*(1-fee))) - f1`
* and `f1 = (1 - 0.5*fee) / (1 - fee)`. The trading fee is charged because
* depositing one asset is equivalent to swapping half for the other asset,
* then making a proportional deposit.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param lptAMMBalance Current total LP token supply.
* @param amount Asset1 amount to deposit.
* @param lpTokensDepositMin Optional minimum LP tokens to receive; `tecAMM_FAILED` if not met.
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleDeposit(
@@ -182,16 +304,22 @@ private:
std::optional<STAmount> const& lpTokensDepositMin,
std::uint16_t tfee);
/** Single asset deposit (Asset1In, LPTokens) by the tokens.
* The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amount max asset1 to deposit
* @param lptAMMBalance current AMM LPT balance
* @param lpTokensDeposit amount of tokens to deposit
* @param tfee trading fee in basis points
* @return
/** Single-asset deposit targeting a specific LP token quantity (`tfOneAssetLPToken`).
*
* Solves the single-deposit formula for the required asset1 input given a
* desired LP token output (`lpTokensDeposit`). Fails with `tecAMM_FAILED`
* if the computed asset1 input would exceed the caller's stated maximum
* (`amount`). The trading fee is charged for the same reason as
* `singleDeposit`.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount Maximum asset1 the depositor will provide.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokensDeposit Exact LP token amount the depositor targets.
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleDepositTokens(
@@ -203,16 +331,24 @@ private:
STAmount const& lpTokensDeposit,
std::uint16_t tfee);
/** Single asset deposit (Asset1In, EPrice) with two constraints.
* The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amount requested asset1 deposit amount
* @param lptAMMBalance current AMM LPT balance
* @param ePrice maximum effective price
* @param tfee
* @return
/** Single-asset deposit with an effective-price ceiling (`tfLimitLPToken`).
*
* Accepts at most `amount` of asset1, subject to the constraint that the
* effective price EP = asset1_in / LP_out must not exceed `ePrice`. Two-pass
* algorithm: if a non-zero `amount` is given and its EP ≤ `ePrice`, that
* deposit is used directly. Otherwise, the exact asset1 and LP token amounts
* that satisfy EP = `ePrice` are derived by solving the quadratic form of
* the single-deposit equation (the derivation is in the `.cpp` inline
* comments). The trading fee is charged as for `singleDeposit`.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount Optional maximum asset1 amount (zero means unconstrained).
* @param lptAMMBalance Current total LP token supply.
* @param ePrice Maximum acceptable effective price (asset1/LP token).
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleDepositEPrice(
@@ -224,13 +360,22 @@ private:
STAmount const& ePrice,
std::uint16_t tfee);
/** Equal deposit in empty AMM state (LP tokens balance is 0)
* @param view
* @param ammAccount
* @param amount requested asset1 deposit amount
* @param amount2 requested asset2 deposit amount
* @param tfee
* @return
/** Bootstrap a zero-balance AMM pool (`tfTwoAssetIfEmpty`).
*
* Only valid when `lptAMMBalance == 0`. Computes the initial LP token
* supply as `sqrt(amount * amount2)` via `ammLPTokens`, seeds the pool with
* both assets, and issues the resulting tokens to the depositor. After
* `applyGuts` commits this, `initializeFeeAuctionVote` grants the
* bootstrapping LP their initial auction-slot and voting position.
*
* @param view Sandbox to mutate.
* @param ammAccount AMM pseudo-account ID.
* @param amount Asset1 amount to seed the pool.
* @param amount2 Asset2 amount to seed the pool.
* @param lptIssue Asset descriptor for the LP token to be issued.
* @param tfee Initial trading fee (from `sfTradingFee` in the transaction,
* or 0 if absent); stored by `initializeFeeAuctionVote`.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
equalDepositInEmptyState(

View File

@@ -4,56 +4,118 @@
namespace xrpl {
/** AMMVote implements AMM vote Transactor.
* This transactor allows for the TradingFee of the AMM instance be a votable
* parameter. Any account (LP) that holds the corresponding LPTokens can cast
* a vote using the new AMMVote transaction. VoteSlots array in ltAMM object
* keeps track of upto eight active votes (VoteEntry) for the instance.
* VoteEntry contains:
* Account - account id that cast the vote.
* FeeVal - proposed fee in basis points.
* VoteWeight - LPTokens owned by the account in basis points.
* TradingFee is calculated as sum(VoteWeight_i * fee_i)/sum(VoteWeight_i).
* Every time AMMVote transaction is submitted, the transactor
* - Fails the transaction if the account doesn't hold LPTokens
* - Removes VoteEntry for accounts that don't hold LPTokens
* - If there are fewer than eight VoteEntry objects then add new VoteEntry
* object for the account.
* - If all eight VoteEntry slots are full, then remove VoteEntry that
* holds less LPTokens than the account. If all accounts hold more
* LPTokens then fail transaction.
* - If the account already holds a vote, then update VoteEntry.
* - Calculate and update TradingFee.
* @see [XLS30d:Governance: Trading Fee Voting
* Mechanism](https://github.com/XRPLF/XRPL-Standards/discussions/78)
/** Governance transactor that lets liquidity providers vote on an AMM's trading fee.
*
* Each LP submits a preferred `sfTradingFee` weighted by their share of the
* pool's total `LPToken` supply. The AMM object stores up to
* `kVOTE_MAX_SLOTS` (8) `VoteEntry` records; the resulting fee is recomputed
* as a capital-weighted average `sum(fee_i * tokens_i) / sum(tokens_i)` over
* all active slots after every vote.
*
* **Slot management:** stale entries (LPs that no longer hold tokens) are
* pruned on every vote. When all eight slots are occupied and the voter is
* not already present, the entry with the fewest tokens is a candidate for
* eviction — but only if the incoming LP holds strictly more tokens, or equal
* tokens and proposes a higher fee. Ties are broken deterministically by
* account ID. If no slot can be displaced, the transaction succeeds but does
* not alter the slot array.
*
* **Auction slot coupling:** after updating `sfTradingFee`, the transactor
* also propagates a new `sfDiscountedFee` (`tradingFee /
* kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`) into `sfAuctionSlot` if one is
* present, keeping the auction winner's price advantage in sync with
* governance decisions. When either fee rounds to zero the field is removed
* rather than stored as zero.
*
* @see [XLS-30d: AMM fee-vote governance](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
class AMMVote : public Transactor
{
public:
/** Uses standard fee and sequence-number consequences; no custom scaling. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit AMMVote(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Amendment gate for AMM vote transactions.
*
* Returns `false` (causing `invokePreflight` to emit `temDISABLED`) when
* the core AMM amendment (`featureAMM` + `fixUniversalNumber`) is not
* enabled. Additionally requires `featureMPTokensV2` when either pool
* asset is an `MPTIssue`.
*
* @param ctx Preflight context providing the active rule set.
* @return `true` if the transaction may proceed; `false` to disable it.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Stateless validation of the vote transaction fields.
*
* Verifies that the asset pair is structurally coherent via
* `invalidAMMAssetPair` and that `sfTradingFee` does not exceed
* `kTRADING_FEE_THRESHOLD` (1000 = 1%). No ledger access is performed.
*
* @param ctx Preflight context carrying the transaction and rule set.
* @return `tesSUCCESS`, `temBAD_FEE` if the fee is out of range, or a
* `tem*` code from `invalidAMMAssetPair` for a malformed asset pair.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger checks before the vote is applied.
*
* Confirms that the AMM object exists for the specified asset pair
* (`terNO_AMM`), that the pool is not empty — a zero `sfLPTokenBalance`
* means there are no active LPs (`tecAMM_EMPTY`) — and that the
* submitting account holds a non-zero LPToken balance (`tecAMM_INVALID_TOKENS`).
* An account with no stake in the pool has no standing to influence its fee.
*
* @param ctx Preclaim context providing the read-only ledger view.
* @return `tesSUCCESS` if all checks pass; `terNO_AMM`,
* `tecAMM_EMPTY`, or `tecAMM_INVALID_TOKENS` otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the vote and update the AMM object's fee governance state.
*
* Wraps execution in a `Sandbox` view so all ledger mutations are
* committed atomically only on success. The vote logic (slot pruning,
* eviction, weighted-average fee recalculation, and auction slot
* discount propagation) is delegated to the file-scoped `applyVote`
* helper in the implementation.
*
* @return `tesSUCCESS` on success; `tecINTERNAL` if the AMM SLE
* cannot be peeked (indicates ledger corruption, unreachable under
* normal conditions).
*/
TER
doApply() override;
/** No transaction-specific invariant entries to visit (future work).
*
* @param isDelete Whether the SLE is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No transaction-specific invariants to finalize (future work).
*
* @param tx The applied transaction.
* @param result The TER code returned by `doApply`.
* @param fee The XRP fee charged.
* @param view Read-only view of the post-apply ledger state.
* @param j Journal for diagnostic logging.
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -8,75 +8,151 @@ namespace xrpl {
class Sandbox;
/** AMMWithdraw implements AMM withdraw Transactor.
* The withdraw transaction is used to remove liquidity from the AMM instance
* pool, thus redeeming some share of the pools that one owns in the form
* of LPTokens. If the trader withdraws proportional values of both assets
* without changing their relative pricing, no trading fee is charged on
* the transaction. The trader can specify different combination of
* the fields in the withdrawal.
* LPTokens - transaction assumes proportional withdrawal of pool assets
* for the amount of LPTokens.
* Asset1Out - transaction assumes withdrawal of single asset equivalent
* to the amount specified in Asset1Out.
* Asset1Out and Asset2Out - transaction assumes all assets withdrawal
* with the constraints on the maximum amount of each asset that
* the trader is willing to withdraw.
* Asset1Out and LPTokens - transaction assumes withdrawal of single
* asset specified in Asset1Out proportional to the share represented
* by the amount of LPTokens.
* Asset1Out and EPrice - transaction assumes withdrawal of single
* asset with the following constraints:
* a. Amount of asset1 if specified (not 0) in Asset1Out specifies
* the minimum amount of asset1 that the trader is willing
* to withdraw.
* b. The effective price of asset traded out does not exceed
* the amount specified in EPrice.
* Following updates after a successful transaction:
* The withdrawn asset, if XRP, is transferred from AMM instance account
* to the account that initiated the transaction, thus changing
* the Balance field of each account.
* The withdrawn asset, if token, is balanced between the AMM instance
* account and the issuer account.
* The LPTokens ~ are balanced between the AMM instance account and
* the account that initiated the transaction.
* The pool composition is updated.
* @see [XLS30d:AMMWithdraw
* transaction](https://github.com/XRPLF/XRPL-Standards/discussions/78)
/** Sentinel flag used to enable exact-zero semantics in final-LP-token withdrawals.
*
* When a liquidity provider redeems their entire LP position, rounding in the
* constant-product math can leave dust amounts. `WithdrawAll::Yes` tells
* `equalWithdrawTokens` and `withdraw` to apply exact-zero arithmetic instead
* of ratio arithmetic, ensuring the pool is properly drained to zero.
* `isWithdrawAll` decodes this from the transaction's `tfWithdrawAll` or
* `tfOneAssetWithdrawAll` flag bits.
*/
enum class WithdrawAll : bool { No = false, Yes };
/** Transactor for `AMMWithdraw` transactions (XLS-30d).
*
* Burns LP tokens to return underlying pool assets to the liquidity provider.
* Five mutually exclusive withdrawal modes are supported, selected by a single
* flag bit from `tfWithdrawSubTx`:
*
* | Flag | Fields required | Fee? | Description |
* |------------------------|------------------------------|:----:|-------------|
* | `tfLPToken` | `sfLPTokenIn` | No | Proportional dual-asset withdrawal for a token amount |
* | `tfTwoAsset` | `sfAmount` + `sfAmount2` | No | Proportional dual-asset withdrawal with per-asset caps |
* | `tfSingleAsset` | `sfAmount` | Yes | Single-asset withdrawal by amount |
* | `tfOneAssetLPToken` | `sfAmount` + `sfLPTokenIn` | Yes | Single-asset withdrawal proportional to a token amount |
* | `tfLimitLPToken` | `sfAmount` + `sfEPrice` | Yes | Single-asset withdrawal with effective-price ceiling |
*
* Fee-free modes (`tfLPToken`, `tfTwoAsset`) remove liquidity proportionally
* without disturbing the pool's price ratio. Fee-bearing modes are equivalent
* to an implicit swap followed by a proportional withdrawal; the pool's
* trading fee applies to that swap component.
*
* The `tfTwoAsset` mode computes the largest proportional withdrawal fitting
* within both asset caps, so actual amounts may be less than the stated maxima.
*
* On success: XRP transfers adjust `sfBalance` on both accounts directly; IOU
* and MPT withdrawals move balances through trustlines between the AMM account
* and the LP. LP tokens are burned from the LP's holding.
*
* Two methods — `equalWithdrawTokens` and `withdraw` — are `public static` so
* that `AMMDelete` and `AMMClawback` can invoke the withdrawal machinery without
* constructing a full `AMMWithdraw` transactor instance. All five mode-specific
* helpers are `private`.
*
* @see [XLS-30d AMMWithdraw](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
class AMMWithdraw : public Transactor
{
public:
/** Standard fee/sequence consequences; no special queue-blocking behavior. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit AMMWithdraw(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Gate on the `featureAMM` amendment and, for MPT-backed pools, on
* `featureMPTokensV2`.
*
* Returns `false` if `featureAMM` is not active. Also returns `false` if
* the pool asset pair or any withdrawal amount uses an `MPTIssue` but
* `featureMPTokensV2` has not yet activated.
*
* @param ctx The preflight context providing rules and the transaction.
* @return `true` if the transaction type is permitted under current rules.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the valid transaction flag mask (`tfAMMWithdrawMask`).
*
* The framework uses this to reject any transaction whose flags fall
* outside the set of bits defined for `AMMWithdraw`.
*
* @param ctx Unused; present for the static dispatch interface.
* @return Bitmask of all valid `AMMWithdraw` flags.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate the transaction structure without accessing ledger state.
*
* Enforces:
* - Exactly one flag bit from `tfWithdrawSubTx` is set
* (`std::popcount == 1`); otherwise `temMALFORMED`.
* - Per-mode field constraints (required and forbidden optional fields).
* - Asset-pair validity via `invalidAMMAssetPair`.
* - `sfAmount` and `sfAmount2` denominate different assets.
* - `sfLPTokenIn`, if present, is positive.
* - `sfEPrice`, `sfAmount`, and `sfAmount2` amounts are valid via
* `invalidAMMAmount`.
*
* @param ctx The preflight context providing the transaction and rules.
* @return `tesSUCCESS` on success, or a `tem*` error code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate against current ledger state (read-only).
*
* Runs after signature verification. Key checks:
* - The AMM ledger entry for the declared asset pair must exist;
* absent → `terNO_AMM`.
* - LP token balance must be positive (`tecAMM_EMPTY` if the pool is
* already drained).
* - The caller holds a sufficient LP token balance for the requested
* withdrawal.
* - When `featureAMMClawback` is active: neither pool asset may be
* individually frozen on the LP's account (`tecFROZEN`).
* - MPT-specific authorization via `checkMPTTxAllowed`.
*
* @param ctx The preclaim context providing the read-only ledger view.
* @return `tesSUCCESS` on success, or the appropriate `tec*`/`ter*` code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the withdrawal against a copy-on-write sandbox and commit on success.
*
* Creates a `Sandbox` over `ctx_.view()`, delegates to `applyGuts`, and
* calls `sb.apply(ctx_.rawView())` only if `applyGuts` returns success.
* Any failure leaves the consensus view unmodified.
*
* @return `tesSUCCESS` or the `tec*` error from the selected withdrawal mode.
*/
TER
doApply() override;
/** No-op placeholder for future transaction-specific invariant state.
*
* `AMMWithdraw` currently delegates all invariant checking to the global
* `ValidAMM` invariant checker. This override exists to satisfy the
* `Transactor` interface and is reserved for future per-transaction
* invariants.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op placeholder for future transaction-specific invariant finalization.
*
* Always returns `true`. See `visitInvariantEntry` for rationale.
*
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -85,20 +161,38 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/** Equal-asset withdrawal (LPTokens) of some AMM instance pools
* shares represented by the number of LPTokens .
* The trading fee is not charged.
* @param view
* @param ammAccount
* @param amountBalance current LP asset1 balance
* @param amount2Balance current LP asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param lpTokens current LPT balance
* @param lpTokensWithdraw amount of tokens to withdraw
* @param tfee trading fee in basis points
* @param withdrawAll if withdrawing all lptokens
* @param priorBalance balance before fees
* @return
/** Proportional dual-asset withdrawal for a given LP token amount.
*
* Burns `lpTokensWithdraw` LP tokens and returns both pool assets to
* `account` in the ratio `lpTokensWithdraw / lptAMMBalance`. No trading
* fee is charged because the withdrawal preserves the pool's price ratio.
* When `withdrawAll` is `WithdrawAll::Yes`, exact-zero arithmetic is used
* to prevent dust from rounding errors on full-position redemptions.
*
* This overload is `public static` so `AMMDelete` and `AMMClawback` can
* invoke it without constructing an `AMMWithdraw` transactor. The caller
* supplies `freezeHandling`, `authHandling`, `priorBalance`, and a journal
* that the instance methods obtain from `ctx_` automatically.
*
* @param view Sandbox to mutate; caller commits only on success.
* @param ammSle AMM ledger entry.
* @param account LP account receiving the withdrawn assets.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens LP token balance held by `account`.
* @param lpTokensWithdraw LP tokens to burn.
* @param tfee Pool trading fee in basis points (unused for fee
* charging; passed to amount-adjustment helpers).
* @param freezeHandling Whether to zero out frozen trustline transfers.
* @param authHandling Whether to zero out unauthorized trustline transfers.
* @param withdrawAll `Yes` if the LP is redeeming their entire position.
* @param priorBalance Caller's XRP balance before fee deduction
* (used for reserve checks during asset transfer).
* @param journal For error logging.
* @return A tuple of `{TER, newLPTokenBalance, asset1Withdrawn, asset2Withdrawn}`.
* Returns `{tecINTERNAL, ...}` if an unexpected exception is caught.
*/
static std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
equalWithdrawTokens(
@@ -118,21 +212,38 @@ public:
XRPAmount const& priorBalance,
beast::Journal const& journal);
/** Withdraw requested assets and token from AMM into LP account.
* Return new total LPToken balance and the withdrawn amounts for both
* assets.
* @param view
* @param ammSle AMM ledger entry
* @param ammAccount AMM account
* @param amountBalance current LP asset1 balance
* @param amountWithdraw asset1 withdraw amount
* @param amount2Withdraw asset2 withdraw amount
* @param lpTokensAMMBalance current AMM LPT balance
* @param lpTokensWithdraw amount of lptokens to withdraw
* @param tfee trading fee in basis points
* @param withdrawAll if withdraw all lptokens
* @param priorBalance balance before fees
* @return
/** Transfer withdrawn assets from the AMM account to the LP and burn LP tokens.
*
* Moves `amountWithdraw` of asset1 (and optionally `amount2Withdraw` of
* asset2) from `ammAccount` to `account`, then burns `lpTokensWithdraw`
* tokens. The trading fee is charged on single-asset modes (the caller is
* responsible for passing the correct `tfee`; fee-free callers pass 0).
* When `withdrawAll` is `WithdrawAll::Yes`, exact-zero arithmetic prevents
* dust from rounding errors on full-position redemptions.
*
* This overload is `public static` so `AMMDelete` and `AMMClawback` can
* invoke it without constructing an `AMMWithdraw` transactor. The caller
* supplies `freezeHandling`, `authHandling`, `priorBalance`, and a journal
* that the instance methods obtain from `ctx_` automatically.
*
* @param view Sandbox to mutate; caller commits only on success.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param account LP account receiving the withdrawn assets.
* @param amountBalance Current pool balance of asset1.
* @param amountWithdraw Asset1 amount to transfer to the LP.
* @param amount2Withdraw Asset2 amount to transfer, or absent for
* single-asset withdrawals.
* @param lpTokensAMMBalance Current total LP token supply.
* @param lpTokensWithdraw LP tokens to burn.
* @param tfee Pool trading fee in basis points.
* @param freezeHandling Whether to zero out frozen trustline transfers.
* @param authHandling Whether to zero out unauthorized trustline transfers.
* @param withdrawAll `Yes` if the LP is redeeming their entire position.
* @param priorBalance Caller's XRP balance before fee deduction
* (used for reserve checks during asset transfer).
* @param journal For error logging.
* @return A tuple of `{TER, newLPTokenBalance, asset1Withdrawn, asset2Withdrawn}`.
*/
static std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
withdraw(
@@ -152,6 +263,32 @@ public:
XRPAmount const& priorBalance,
beast::Journal const& journal);
/** Delete the AMM instance account and its `ltAMM` entry if no LP tokens remain.
*
* After burning LP tokens, if `lpTokenBalance` has reached zero the AMM
* pseudo-account and its associated ledger entry are orphaned objects.
* This method detects that condition and calls `deleteAMMAccount`, which
* removes those objects before the sandbox is committed.
*
* If `deleteAMMAccount` returns `tecINCOMPLETE` (partial deletion — more
* objects remain to clean up), the LP token balance is still updated on
* `ammSle` so the next transaction can continue the teardown. If the
* balance is non-zero, only the balance field is updated and no deletion
* is attempted.
*
* Called by `applyGuts`, `AMMDelete`, and `AMMClawback` after every
* successful token burn.
*
* @param sb Sandbox staging all mutations; caller commits on success.
* @param ammSle AMM ledger entry (mutable); its `sfLPTokenBalance`
* is updated unless full deletion occurs.
* @param lpTokenBalance Post-burn LP token balance.
* @param asset1 First pool asset (identifies the AMM for deletion).
* @param asset2 Second pool asset (identifies the AMM for deletion).
* @param journal For error logging.
* @return `{TER, true}` on success or partial cleanup (`tecINCOMPLETE`);
* `{tec*, false}` if an unexpected deletion error occurs.
*/
static std::pair<TER, bool>
deleteAMMAccountIfEmpty(
Sandbox& sb,
@@ -162,20 +299,37 @@ public:
beast::Journal const& journal);
private:
/** Dispatch to the mode-specific withdrawal helper and commit AMM state.
*
* Reads the current pool balances, selects the appropriate private method
* based on `tfWithdrawSubTx` flag bits, executes it, then calls
* `deleteAMMAccountIfEmpty` to clean up orphaned objects if the pool is
* fully drained. On `tesSUCCESS`, updates `sfLPTokenBalance` on the AMM
* ledger entry.
*
* @param view The sandbox to mutate; committed by the caller only on success.
* @return `{TER, true}` on success; `{tec*, false}` on failure.
*/
std::pair<TER, bool>
applyGuts(Sandbox& view);
/** Withdraw requested assets and token from AMM into LP account.
* Return new total LPToken balance.
* @param view
* @param ammSle AMM ledger entry
* @param ammAccount AMM account
* @param amountBalance current LP asset1 balance
* @param amountWithdraw asset1 withdraw amount
* @param amount2Withdraw asset2 withdraw amount
* @param lpTokensAMMBalance current AMM LPT balance
* @param lpTokensWithdraw amount of lptokens to withdraw
* @return
/** Instance-method wrapper around the public static `withdraw` overload.
*
* Forwards to the static overload with `FreezeHandling::ZeroIfFrozen`,
* `AuthHandling::ZeroIfUnauthorized`, `isWithdrawAll(ctx_.tx)`,
* `preFeeBalance_`, and `j_` sourced from the instance's `ApplyContext`.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amountWithdraw Asset1 amount to transfer to the LP.
* @param amount2Withdraw Asset2 amount to transfer, or absent for
* single-asset withdrawals.
* @param lpTokensAMMBalance Current total LP token supply.
* @param lpTokensWithdraw LP tokens to burn.
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
withdraw(
@@ -189,18 +343,24 @@ private:
STAmount const& lpTokensWithdraw,
std::uint16_t tfee);
/** Equal-asset withdrawal (LPTokens) of some AMM instance pools
* shares represented by the number of LPTokens .
* The trading fee is not charged.
* @param view
* @param ammAccount
* @param amountBalance current LP asset1 balance
* @param amount2Balance current LP asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param lpTokens current LPT balance
* @param lpTokensWithdraw amount of tokens to withdraw
* @param tfee trading fee in basis points
* @return
/** Instance-method wrapper around the public static `equalWithdrawTokens` overload.
*
* Forwards to the static overload with `FreezeHandling::ZeroIfFrozen`,
* `AuthHandling::ZeroIfUnauthorized`, `isWithdrawAll(ctx_.tx)`,
* `preFeeBalance_`, and `ctx_.journal` sourced from the instance's
* `ApplyContext`. No trading fee is charged.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens LP token balance held by the caller's account.
* @param lpTokensWithdraw LP tokens to burn.
* @param tfee Pool trading fee in basis points (passed to
* amount-adjustment helpers; not charged).
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
equalWithdrawTokens(
@@ -214,18 +374,27 @@ private:
STAmount const& lpTokensWithdraw,
std::uint16_t tfee);
/** Withdraw both assets (Asset1Out, Asset2Out) with the constraints
* on the maximum amount of each asset that the trader is willing
* to withdraw. The trading fee is not charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param amount2Balance current AMM asset2 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount asset1 withdraw amount
* @param amount2 max asset2 withdraw amount
* @param tfee trading fee in basis points
* @return
/** Proportional dual-asset withdrawal with per-asset maximum caps (`tfTwoAsset`).
*
* Computes the largest proportional withdrawal (as a fraction of
* `lptAMMBalance`) that fits within both `amount` (asset1 cap) and `amount2`
* (asset2 cap). Because the withdrawal is proportional, the pool's price
* ratio is preserved and no trading fee is charged. The actual amounts
* withdrawn may be less than the stated maximums.
*
* Fails with `tecAMM_FAILED` if neither asset cap yields a feasible solution.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param amount2Balance Current pool balance of asset2.
* @param lptAMMBalance Current total LP token supply.
* @param amount Maximum asset1 the LP is willing to withdraw.
* @param amount2 Maximum asset2 the LP is willing to withdraw.
* @param tfee Pool trading fee in basis points (passed through
* for amount-adjustment rounding; not charged).
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
equalWithdrawLimit(
@@ -239,15 +408,22 @@ private:
STAmount const& amount2,
std::uint16_t tfee);
/** Single asset withdrawal (Asset1Out) equivalent to the amount specified
* in Asset1Out. The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount asset1 withdraw amount
* @param tfee trading fee in basis points
* @return
/** Single-asset withdrawal by specified amount (`tfSingleAsset`).
*
* Computes the LP tokens to burn via `lpTokensIn(amountBalance, amount,
* lptAMMBalance, tfee)`. Single-asset withdrawal is mathematically
* equivalent to a swap followed by a proportional withdrawal, so the pool's
* trading fee applies. Fails with `tecAMM_INVALID_TOKENS` (under
* `fixAMMv1_3`) if the computed token amount rounds to zero.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param lptAMMBalance Current total LP token supply.
* @param amount Asset1 amount to withdraw.
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleWithdraw(
@@ -259,16 +435,23 @@ private:
STAmount const& amount,
std::uint16_t tfee);
/** Single asset withdrawal (Asset1Out, LPTokens) proportional
* to the share specified by tokens. The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount asset1 withdraw amount
* @param lpTokensWithdraw amount of tokens to withdraw
* @param tfee trading fee in basis points
* @return
/** Single-asset withdrawal for a specified LP token amount (`tfOneAssetLPToken`).
*
* Adjusts `lpTokensWithdraw` via `adjustLPTokensIn`, then solves for the
* asset1 output implied by burning exactly that many tokens, subject to a
* minimum of `amount` (the caller's floor). The trading fee is charged.
* Fails with `tecAMM_INVALID_TOKENS` (under `fixAMMv1_3`) if the adjusted
* token amount rounds to zero.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param lptAMMBalance Current total LP token supply.
* @param amount Minimum asset1 the LP is willing to receive.
* @param lpTokensWithdraw LP tokens to burn.
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleWithdrawTokens(
@@ -281,16 +464,27 @@ private:
STAmount const& lpTokensWithdraw,
std::uint16_t tfee);
/** Withdraw single asset (Asset1Out, EPrice) with two constraints.
* The trading fee is charged.
* @param view
* @param ammAccount
* @param amountBalance current AMM asset1 balance
* @param lptAMMBalance current AMM LPT balance
* @param amount asset1 withdraw amount
* @param ePrice maximum asset1 effective price
* @param tfee trading fee in basis points
* @return
/** Single-asset withdrawal with an effective-price ceiling (`tfLimitLPToken`).
*
* Enforces two simultaneous constraints: a minimum output amount (`amount`,
* or zero meaning unconstrained) and a maximum effective price per LP token
* burned (`ePrice`). This mode is the closest analogue to a limit order at
* exit time — the transaction fails with `tecAMM_FAILED` if the current
* pool price exceeds `ePrice`. The trading fee is charged.
*
* Two-pass algorithm: if a non-zero `amount` satisfies the price constraint
* it is used directly; otherwise the exact asset1 and token amounts that
* satisfy `EP = ePrice` are derived analytically.
*
* @param view Sandbox to mutate.
* @param ammSle AMM ledger entry.
* @param ammAccount AMM pseudo-account ID.
* @param amountBalance Current pool balance of asset1.
* @param lptAMMBalance Current total LP token supply.
* @param amount Minimum asset1 to withdraw (zero = unconstrained).
* @param ePrice Maximum effective price (asset1 per LP token burned).
* @param tfee Pool trading fee in basis points.
* @return `{tesSUCCESS, newLPTokenBalance}` or `{tec*, STAmount{}}`.
*/
std::pair<TER, STAmount>
singleWithdrawEPrice(
@@ -303,7 +497,12 @@ private:
STAmount const& ePrice,
std::uint16_t tfee);
/** Check from the flags if it's withdraw all */
/** Decode the `tfWithdrawAll` / `tfOneAssetWithdrawAll` flag bits.
*
* @param tx The transaction to inspect.
* @return `WithdrawAll::Yes` if either full-redemption flag is set;
* `WithdrawAll::No` otherwise.
*/
static WithdrawAll
isWithdrawAll(STTx const& tx);
};

View File

@@ -5,6 +5,23 @@
namespace xrpl {
/** Transactor for the OfferCancel transaction type.
*
* Removes a standing DEX offer identified by its original sequence number.
* The implementation is intentionally thin: field validation is minimal,
* and the actual ledger teardown — unlinking from the owner directory,
* removing the order-book directory entry, updating owner counts — is
* delegated entirely to the shared `offerDelete` helper.
*
* Cancellation is idempotent: if the target offer no longer exists
* (consumed by a trade, previously cancelled, or expired), `doApply`
* returns `tesSUCCESS` without modifying any ledger state. The fee is
* still charged — the transaction was valid and processed.
*
* `kCONSEQUENCES_FACTORY` is `Normal` because cancellation never reserves
* owner reserves or locks funds beyond the transaction fee itself, so the
* framework can model its consequences without per-transaction computation.
*/
class OfferCancel : public Transactor
{
public:
@@ -14,21 +31,67 @@ public:
{
}
/** Validate static transaction fields before ledger access.
*
* Rejects the transaction with `temBAD_SEQUENCE` if `sfOfferSequence`
* is zero. No valid offer can carry sequence number zero, so a zero
* value is always a client error. All other field checks (account,
* fee, flags, signature) are handled by `preflight1`/`preflight2` in
* the base-class pipeline.
*
* @param ctx Preflight context carrying the transaction and rules.
* @return `tesSUCCESS` if the sequence field is non-zero;
* `temBAD_SEQUENCE` otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Check ledger state before applying the cancellation.
*
* Reads the submitter's account entry and enforces a temporal ordering
* invariant: `sfOfferSequence` must be strictly less than the account's
* current sequence number. If the offer sequence is greater than or
* equal to the account sequence, the offer could not yet exist on the
* ledger, and the transaction is rejected.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the account exists and the sequence ordering
* is valid; `terNO_ACCOUNT` if the submitter's account is not found
* (highly unusual at this stage); `temBAD_SEQUENCE` if
* `sfOfferSequence` is not strictly less than the account's current
* sequence number.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Remove the target offer from the ledger.
*
* Resolves the offer via `keylet::offer(account_, offerSequence)`. If
* the offer exists, delegates removal to `offerDelete`, which unlinks
* it from the owner and order-book directories and adjusts the owner
* count. If the offer is not found, returns `tesSUCCESS` without any
* state changes — the offer may have already been consumed by a
* crossing trade, cancelled, or expired.
*
* @return Result of `offerDelete` if the offer was found;
* `tesSUCCESS` if the offer was already absent from the ledger;
* `tefINTERNAL` (unreachable in practice) if the account SLE is
* missing.
*/
TER
doApply() override;
/** Per-entry invariant visitor (no-op; reserved for future work). */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize invariant checks (no-op; reserved for future work).
*
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,3 +1,13 @@
/** @file
* Declares the OfferCreate transactor for the XRPL decentralized exchange.
*
* OfferCreate processes `ttOFFER_CREATE` transactions: it validates offer
* fields, optionally cancels a pre-existing offer, attempts immediate crossing
* against resting orders via the payment engine, and — if any unfilled amount
* remains — writes a new offer ledger entry into the appropriate order-book
* directory.
*/
#pragma once
#include <xrpl/protocol/Quality.h>
@@ -8,44 +18,135 @@ namespace xrpl {
class PaymentSandbox;
class Sandbox;
/** Transactor specialized for creating offers in the ledger. */
/** Transactor for `ttOFFER_CREATE` transactions on the XRPL DEX.
*
* Handles the full lifecycle of offer creation: structural validation in
* `preflight`, ledger-state checks in `preclaim`, and the three-stage
* `doApply` sequence of (optional) offer cancellation, crossing via the
* payment engine (`flowCross`), and conditional placement of any residual
* offer on the order book.
*
* Uses `ConsequencesFactoryType::Custom` so the transaction queue can
* accurately bound the maximum XRP a queued offer could spend (see
* `makeTxConsequences`).
*
* @see OfferCancel for the simpler remove-only counterpart.
*/
class OfferCreate : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Custom;
/** Construct a Transactor subclass that creates an offer in the ledger. */
/** Construct the transactor for the given apply context. */
explicit OfferCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Compute the maximum XRP this offer could spend, for queue accounting.
*
* If `sfTakerGets` is native (XRP), returns that amount as the upper
* bound on XRP consumed. For IOU-only offers, the XRP spend is zero.
* This prevents over-reserving queue capacity for non-XRP offers.
*
* @param ctx Preflight context supplying the transaction fields.
* @return `TxConsequences` carrying the calculated maximum XRP spend.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/** Gate the transaction on the protocol amendments it requires.
*
* Rejects the transaction (`temDISABLED`) if `sfDomainID` is present but
* `featurePermissionedDEX` is not enabled, or if either `sfTakerPays` or
* `sfTakerGets` carries an `MPTIssue` without `featureMPTokensV2`.
*
* @param ctx Preflight context supplying transaction fields and rules.
* @return `true` if all required amendments are active; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the bitmask of flags accepted by this transaction type.
*
* The base mask is `tfOfferCreateMask`. When `featurePermissionedDEX` is
* *not* active, `tfHybrid` is OR-ed in so that `preflight0` rejects any
* transaction that sets it.
*
* @param ctx Preflight context supplying the active amendment rules.
* @return 32-bit mask of permitted flag bits.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Enforce constraints beyond those of the Transactor base class. */
/** Validate the structural integrity of the transaction fields.
*
* Checks (in order): `tfHybrid` requires `sfDomainID`; mutual exclusivity
* of `tfImmediateOrCancel` and `tfFillOrKill`; non-zero expiration if
* present; non-zero cancel sequence if present; legal amounts for both
* sides; no XRP-for-XRP or same-asset offers; no bad currency codes; and
* native/issuer field consistency. All rejections are `tem*` codes — no
* fee is charged.
*
* @param ctx Preflight context supplying transaction fields and rules.
* @return `tesSUCCESS` on success, or a `tem*` code on any field error.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Enforce constraints beyond those of the Transactor base class. */
/** Check ledger-state preconditions against a read-only view.
*
* Verifies: the submitting account exists; neither asset is globally
* frozen; the account holds sufficient funds to back the offer
* (`tecUNFUNDED_OFFER`); the optional cancel-sequence is valid; the offer
* has not already expired; the account is authorized to receive
* `sfTakerPays` (via `checkAcceptAsset`); and — for domain offers — the
* account is a member of the referenced `PermissionedDEX` domain.
*
* @param ctx Preclaim context supplying the read-only ledger view.
* @return `tesSUCCESS` on success, or an appropriate `ter*`/`tec*` code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Precondition: fee collection is likely. Attempt to create the offer. */
/** Execute the offer: cancel, cross, and optionally place on the book.
*
* Allocates two sandboxes and delegates to `applyGuts`. Commits the
* primary sandbox when the offer is placed or fully crossed; commits only
* the cancel sandbox when a Fill-or-Kill offer cannot be filled (so that
* stale-offer housekeeping from crossing is still recorded).
*
* @return `tesSUCCESS` or a `tec*` code; never `tem*`/`tef*`.
*/
TER
doApply() override;
/** Accumulate per-SLE state for offer-specific invariant checks.
*
* Called once per modified ledger entry after `doApply` completes. No
* transaction-specific invariants are currently enforced here.
*
* @param isDelete `true` if the entry is being removed from the ledger.
* @param before SLE state before the transaction; `nullptr` for new entries.
* @param after SLE state after the transaction; `nullptr` for deletions.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize offer-specific invariant checks after all entries are visited.
*
* No transaction-specific invariants are currently enforced. Always
* returns `true`.
*
* @param tx The transaction being applied.
* @param result The TER produced by `doApply`.
* @param fee The fee charged for this transaction.
* @param view The read-only ledger view after application.
* @param j Journal for diagnostic logging.
* @return `true` — no invariant violations detected.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -55,10 +156,43 @@ public:
beast::Journal const& j) override;
private:
/** Execute the core offer logic against a pair of sandboxes.
*
* Handles cancellation of any existing offer nominated by
* `sfOfferSequence`, tick-size rounding, crossing via `flowCross`,
* Fill-or-Kill / Immediate-or-Cancel evaluation, reserve checks, and
* placement of any residual offer into the order-book directory. If the
* transaction should be committed with full state changes, returns
* `{TER, true}`; if only `viewCancel` should be committed (e.g., a
* killed Fill-or-Kill), returns `{TER, false}`.
*
* @param view Primary sandbox for the complete transaction result.
* @param viewCancel Secondary sandbox committed on Fill-or-Kill failure,
* preserving stale-offer removals made during crossing.
* @return Pair of `{TER, bool}` where the bool selects which sandbox to
* commit.
*/
std::pair<TER, bool>
applyGuts(Sandbox& view, Sandbox& viewCancel);
// Determine if we are authorized to hold the asset we want to get.
/** Verify that the account is permitted to receive the specified asset.
*
* Only meaningful for custom currencies (asserts XRP is never passed).
* For IOU assets: checks issuer existence and, when `lsfRequireAuth` is
* set on the issuer, verifies that a trust line exists and carries the
* appropriate authorization flag (`lsfLowAuth` or `lsfHighAuth` per
* canonical account ordering). Also rejects deep-frozen trust lines
* (`lsfLowDeepFreeze`/`lsfHighDeepFreeze`). For MPT assets: delegates to
* `requireAuth` with `WeakAuth` semantics (an `MPToken` holder entry need
* not pre-exist).
*
* @param view Read-only ledger view.
* @param flags Apply flags (controls `ter*` vs `tec*` error selection).
* @param id Account that would receive the asset.
* @param j Journal for diagnostic logging.
* @param asset The IOU or MPT asset to be received.
* @return `tesSUCCESS`, or `ter*`/`tec*` if the account lacks authority.
*/
static TER
checkAcceptAsset(
ReadView const& view,
@@ -67,7 +201,24 @@ private:
beast::Journal const j,
Asset const& asset);
// Use the payment flow code to perform offer crossing.
/** Cross the offer against resting orders using the payment flow engine.
*
* Delegates to the same `flow()` function used by `Payment` transactions,
* inverting `TakerPays`/`TakerGets` so the offer creator acts as a taker.
* Constructs a quality threshold to enforce the passive flag; for
* IOU-to-IOU offers injects an XRP intermediate path to enable two-book
* crossing. For `tfSell` passes maximum delivery limits to accept any
* amount of the `Gets` asset. After crossing, computes the residual offer
* amounts at the original quality, factoring out gateway transfer rates.
* Stale offers encountered during crossing are removed in both sandboxes.
*
* @param psb Primary `PaymentSandbox` (wraps the main `Sandbox`).
* @param psbCancel Cancel-only `PaymentSandbox` (wraps `sbCancel`).
* @param takerAmount Inverted offer amounts from the taker's perspective.
* @param domainID Optional permissioned-DEX domain to restrict crossing.
* @return Pair of `{TER, Amounts}` where `Amounts` is the residual offer
* after crossing (zero if fully filled).
*/
std::pair<TER, Amounts>
flowCross(
PaymentSandbox& psb,
@@ -75,9 +226,28 @@ private:
Amounts const& takerAmount,
std::optional<uint256> const& domainID);
/** Format an `STAmount` as a human-readable string for logging. */
static std::string
formatAmount(STAmount const& amount);
/** Index a hybrid domain offer into the open (non-domain) order book.
*
* Sets `lsfHybrid` on the offer SLE, creates a second book-directory
* entry using `std::nullopt` as the domain, and records that entry's
* key and page node in `sfAdditionalBooks` on the offer. This dual
* indexing lets open-market order-book walks find and consume domain
* offers. Only called when `tfHybrid` is set.
*
* @param sb Main sandbox holding the offer SLE.
* @param sleOffer The offer ledger entry to be dually indexed.
* @param offerIndex Keylet of the offer entry.
* @param saTakerPays Amount the taker pays (book sort key).
* @param saTakerGets Amount the taker gets (book sort key).
* @param setDir Callback that writes the book-directory key and
* domain into the offer SLE.
* @return `tesSUCCESS`, or `tecDIR_FULL` if the open book directory is
* at capacity.
*/
TER
applyHybrid(
Sandbox& sb,

Some files were not shown because too many files have changed in this diff Show More