mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-02 16:26:48 +00:00
part 2
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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;
|
||||
/** @} */
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (0–1000).
|
||||
*/
|
||||
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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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 0–50,000 representing 0–50%
|
||||
* (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^63−1)
|
||||
* 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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 4–23 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
|
||||
{
|
||||
|
||||
@@ -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 1–3), 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>;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 quality–output line (`-cfee / poolGets` for AMM; 0 for CLOB). */
|
||||
Number m_;
|
||||
// intercept
|
||||
/** Intercept of the quality–output 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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 (0–50,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 (0–50,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 1–11 ("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 12–13 are reserved gaps.
|
||||
* Codes 10001–10004 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 55–62 = `offset + 97`; bits 0–53 = 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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 1–3 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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 2–6 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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 1–3 byte length header followed by the raw bytes:
|
||||
* 0–192 bytes use a 1-byte header; 193–12,480 use 2 bytes; 12,481–918,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 (1–255).
|
||||
* @param name Per-type field index (1–255).
|
||||
* @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 (1–255).
|
||||
* @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; 193–240 → 2
|
||||
* bytes; 241–254 → 3 bytes.
|
||||
*
|
||||
* @param b1 First byte of the VL header (0–254).
|
||||
* @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 (0–192 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 (193–12,480 range).
|
||||
*
|
||||
* Formula: `193 + (b1 - 193) * 256 + b2`.
|
||||
*
|
||||
* @param b1 First header byte (193–240).
|
||||
* @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,481–918,744 range).
|
||||
*
|
||||
* Formula: `12481 + (b1 - 241) * 65536 + b2 * 256 + b3`.
|
||||
*
|
||||
* @param b1 First header byte (241–254).
|
||||
* @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 1–3 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 1–3 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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 1–32569 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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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))))
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 1–32); 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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 0–18; 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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user