mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 11:37:07 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91985dc320 |
No files matched your search
@@ -4,18 +4,24 @@ This file provides guidance to AI coding agents (Claude Code, and other AGENTS.m
|
||||
|
||||
## Build
|
||||
|
||||
For the build steps, CMake options, and protocol codegen commands, see [BUILD.md](./BUILD.md) (`## Steps`, `## Options`, `## Code generation`). Nix development shells are available in the repo (not required) — see [docs/build/nix.md](./docs/build/nix.md) for setup and variants.
|
||||
Required on Linux/macOS: use the Nix devshell, which sets up the compiler, Conan, ccache, and (optionally) Rust automatically.
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
For alternate devshell variants (specific compiler, no-compiler, coverage), see [docs/build/nix.md](./docs/build/nix.md). For the manual build steps, CMake options, and protocol codegen commands, see [BUILD.md](./BUILD.md) (`## Steps`, `## Options`, `## Code generation`).
|
||||
|
||||
Rust crate tests (independent of the CMake build): `cargo test --manifest-path crates/Cargo.toml --workspace` (CI uses `cargo nextest`).
|
||||
|
||||
## Testing
|
||||
|
||||
The legacy unit tests under `src/test/` are a custom framework (derived from Beast, not Boost.Test/GTest/Catch) built into the `xrpld` binary; see [CONTRIBUTING.md](./CONTRIBUTING.md#unit-tests) for the basic invocation. The gtest suites under `src/tests/` build into the separate `xrpl_tests` and `xrpld_tests` binaries (run with `--gtest_filter=...`). Notes not covered there:
|
||||
Unit tests are a custom framework built into the `xrpld` binary itself (not Boost.Test/GTest/Catch); see [CONTRIBUTING.md](./CONTRIBUTING.md#unit-tests) for the basic invocation. Notes not covered there:
|
||||
|
||||
- A suite's `--unittest` name is built from the arguments to its `BEAST_DEFINE_TESTSUITE`/`BEAST_DEFINE_TESTSUITE_PRIO` macro (usually at the bottom of the test file), in reverse order and joined with `.`: `BEAST_DEFINE_TESTSUITE(Credentials, app, xrpl)` → `xrpl.app.Credentials`.
|
||||
- `--unittest-arg=<string>` passes one string to every suite that runs; a suite reads it with `arg()` and interprets it itself. Only a few suites use it (e.g. `TheoreticalQuality`, the nodestore `import` test); for the rest it has no effect.
|
||||
- `--unittest-arg` does nothing — don't use it.
|
||||
- Tests that run offline in under a minute should be automatic `--unittest` suites; anything else is a manual/integration test.
|
||||
- New tests should be written using `gtest` under `src/tests/` unless that isn't possible, in which case fall back to the legacy Beast framework under `src/test/` (see [src/test/AGENTS.md](./src/test/AGENTS.md) for conventions specific to that directory). `tests/` (top-level) holds integration tests exercised against `libxrpl`/`xrpld`.
|
||||
- New tests should be written using `gtest` under `src/tests/` unless that isn't possible, in which case fall back to the legacy Beast framework under `src/test/`. `tests/` (top-level) holds integration tests exercised against `libxrpl`/`xrpld`.
|
||||
|
||||
## Lint/Format
|
||||
|
||||
@@ -23,14 +29,14 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md#pre-commit-hooks) for `pre-commit` setup
|
||||
|
||||
## Code Style
|
||||
|
||||
New file placement and header levelization: see [CONTRIBUTING.md](./CONTRIBUTING.md#before-making-a-pull-request). Braces, whitespace, member order, and other conventions: see [docs/CodingStyle.md](./docs/CodingStyle.md). `XRPL_ASSERT`/`UNREACHABLE` contracts: see [CONTRIBUTING.md](./CONTRIBUTING.md#contracts-and-instrumentation). Commit messages: see [CONTRIBUTING.md](./CONTRIBUTING.md#good-commit-messages). New public functions/methods need a Doxygen-style comment.
|
||||
|
||||
Comments should explain _why_, not _what_/_how_ — the code already shows that. Only describe what/how when the code itself would otherwise be confusing (a non-obvious workaround, a subtle invariant, a surprising constraint).
|
||||
New file placement and header levelization: see [CONTRIBUTING.md](./CONTRIBUTING.md#before-making-a-pull-request). Braces, whitespace, member order, and other conventions: see [docs/CodingStyle.md](./docs/CodingStyle.md). `XRPL_ASSERT`/`UNREACHABLE` contracts: see [CONTRIBUTING.md](./CONTRIBUTING.md#contracts-and-instrumentation). Commit messages: see [CONTRIBUTING.md](./CONTRIBUTING.md#good-commit-messages).
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the directory-by-directory map of the codebase.
|
||||
Paths below reflect the current layout; update this section if modularization moves a subsystem to a different directory.
|
||||
|
||||
## Keeping docs current
|
||||
|
||||
When you add or change a convention, or touch a subsystem that has its own `AGENTS.md`, `README.md`, or `ARCHITECTURE.md`, update that documentation in the same change rather than leaving it stale.
|
||||
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths). `tx/transactors/` has one file per transaction type, grouped by subsystem: `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `account/`, `system/`. Any change to transaction-processing behavior must be gated behind an Amendment.
|
||||
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
|
||||
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
|
||||
- `src/tests/` — unit tests for `libxrpl` written in `gtest`, gradually replacing the `src/test` equivalents.
|
||||
- `crates/` — a Rust workspace (only built with `-Dxrpld -Drust=ON`) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
|
||||
@@ -1,9 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
Paths below reflect the current layout; update this doc if modularization moves a subsystem to a different directory.
|
||||
|
||||
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths — see [src/libxrpl/tx/AGENTS.md](./src/libxrpl/tx/AGENTS.md) for amendment-gating conventions). `tx/transactors/` has one file per transaction type, grouped by subsystem: `payment/`, `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `proposal/`, `account/`, `system/`.
|
||||
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
|
||||
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
|
||||
- `src/tests/` — gtest unit tests, with `libxrpl/` and `xrpld/` subtrees, gradually replacing the `src/test` equivalents.
|
||||
- `crates/` — a Rust workspace (only built with `-Drust=ON`; today only `xrpl_tests` links it, so `-Dtests=ON` is needed to exercise it) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
|
||||
@@ -596,23 +596,16 @@ operator<=>(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
|
||||
// note that the underlying data is stored in big endian, even if the
|
||||
// platform is little endian. This makes the comparison correct.
|
||||
//
|
||||
// FIXME: use std::lexicographical_compare_three_way once support is
|
||||
// added to MacOS.
|
||||
|
||||
auto const ret = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin());
|
||||
|
||||
// a == b
|
||||
if (ret.first == lhs.cend())
|
||||
return std::strong_ordering::equivalent;
|
||||
|
||||
return (*ret.first > *ret.second) ? std::strong_ordering::greater : std::strong_ordering::less;
|
||||
// memcmp compares unsigned bytes in order, and is much faster than a
|
||||
// byte-at-a-time loop.
|
||||
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) <=> 0;
|
||||
}
|
||||
|
||||
template <std::size_t Bits, typename Tag>
|
||||
[[nodiscard]] constexpr bool
|
||||
operator==(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
|
||||
{
|
||||
return (lhs <=> rhs) == 0; // NOLINT(modernize-use-nullptr)
|
||||
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — consensus
|
||||
|
||||
See [README.md](./README.md) for a short pointer, and [docs/consensus.md](../../../docs/consensus.md) for the full consensus design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -265,13 +265,6 @@ canWithdraw(
|
||||
[[nodiscard]] TER
|
||||
canWithdraw(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Pays out a withdrawal from a vault or loan broker pseudo-account.
|
||||
*
|
||||
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
|
||||
* The two differ only when the withdrawal carries a transfer fee; the
|
||||
* difference is the fee and is settled through the issuer.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
doWithdraw(
|
||||
ApplyViewContext ctx,
|
||||
@@ -279,8 +272,7 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -316,20 +316,6 @@ transferRate(ReadView const& view, Asset const& asset);
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Returns the amount a sender must part with for `amount` to reach the
|
||||
* receiver once the issuer takes the transfer fee. Rounds up.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/**
|
||||
* Returns the amount a receiver gets when `amount` leaves the sender and the
|
||||
* issuer takes the transfer fee. Rounds down.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations (Asset-based dispatchers)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — nodestore
|
||||
|
||||
See [README.md](./README.md) for backend and benchmark design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -696,16 +696,6 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
|
||||
STAmount
|
||||
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
|
||||
|
||||
/**
|
||||
* Returns amt * num / den, rounded in the requested direction.
|
||||
*
|
||||
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
|
||||
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
|
||||
* requested direction. Only Upward and Downward are supported.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — shamap
|
||||
|
||||
See [README.md](./README.md) for the SHAMap design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -511,14 +511,9 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
destinationAmount.asset() == sourceAmount.asset(),
|
||||
"xrpl::doWithdraw : delivered and source amounts use the same asset");
|
||||
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
@@ -528,10 +523,9 @@ doWithdraw(
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter =
|
||||
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
@@ -546,33 +540,17 @@ doWithdraw(
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
destinationAmount.asset(),
|
||||
amount.asset(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j) < sourceAmount)
|
||||
j) < amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
|
||||
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A transfer fee applies. Redeem the gross amount with the issuer, then
|
||||
// issue the net amount to the destination.
|
||||
if (sourceAmount != destinationAmount)
|
||||
{
|
||||
auto const issuer = destinationAmount.getIssuer();
|
||||
XRPL_ASSERT(
|
||||
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
|
||||
"xrpl::doWithdraw : transfer fee applies between token holders");
|
||||
|
||||
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
|
||||
}
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects, so resolve the
|
||||
// sponsor against the destination. accountSend can auto-create a holding
|
||||
// for dstAcct; keying on the destination ensures a third-party destination's
|
||||
@@ -584,7 +562,7 @@ doWithdraw(
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# AGENTS.md — ledger/helpers
|
||||
|
||||
A helper that takes an `SLE`/`std::shared_ptr<SLE const>` should `XRPL_ASSERT` that it's non-null and of the expected ledger-entry type at entry, and keep a real runtime check/error-return alongside the assert (asserts compile out in release builds) — the established idiom in this directory is `std::expected<..., TER>`, returning `std::unexpected(tec*)` on failure. Don't invent a new error-handling idiom for this.
|
||||
|
||||
Prefer a single amendment-enabled block and a single disabled block over scattering `rules.enabled(...)` checks through a function, even if the two blocks are similar. When a file or function checks more than one amendment, name local enablement booleans per-amendment (e.g. `fix340Enabled` for `fixCleanup3_4_0`), not a generic `fixEnabled`.
|
||||
|
||||
Only use `UNREACHABLE` for genuinely impossible paths, not to avoid writing a test for one that's reachable but rare. When a branch marked `UNREACHABLE` is excluded from coverage, wrap it in `LCOV_EXCL_START`/`LCOV_EXCL_STOP`.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -552,22 +551,6 @@ transferRate(ReadView const& view, STAmount const& amount)
|
||||
return transferRate(view, amount.asset());
|
||||
}
|
||||
|
||||
STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations
|
||||
@@ -1364,8 +1347,9 @@ directSendNoLimitMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the cost
|
||||
// exactly, matching the payment engine.
|
||||
saActual = addTransferFee(saAmount, rate);
|
||||
// exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
saActual = STAmount(saAmount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1490,8 +1474,9 @@ directSendNoLimitMultiMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the
|
||||
// cost exactly, matching the payment engine.
|
||||
actualSend = addTransferFee(amount, rate);
|
||||
// cost exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
actualSend = STAmount(amount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1763,24 +1763,6 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
|
||||
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
|
||||
bool const roundUp = rounding == Number::RoundingMode::Upward;
|
||||
return amt.asset().visit(
|
||||
[&](Issue const& issue) -> STAmount {
|
||||
if (amt.native())
|
||||
return mulRatio(amt.xrp(), num, den, roundUp);
|
||||
return {mulRatio(amt.iou(), num, den, roundUp), issue};
|
||||
},
|
||||
[&](MPTIssue const& issue) -> STAmount {
|
||||
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
STAmount::isZeroAtScale(int scale) const
|
||||
{
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
# AGENTS.md — tx
|
||||
|
||||
## When an amendment is required
|
||||
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
|
||||
|
||||
A change needs an amendment if it affects transaction processing, ledger objects, or anything else about the binary format or hash of the ledger. An amendment is optional if a change only affects what transactions get proposed for consensus (e.g. fee escalation). Otherwise, don't use one.
|
||||
|
||||
New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
|
||||
|
||||
When adding a new amendment or transaction type, check its interaction with: invariants, fees, Deposit Auth, Batch transaction inclusion/exclusion, Permission Delegation inclusion/exclusion, Freeze/Deep Freeze (IOU) and Lock (MPT), Clawback, Credentials and Permissioned Domain, the case where the submitting account is the asset's issuer, and numeric over/underflow. Stick to existing paradigms rather than inventing new ones — consistency between features matters more than a locally "better" design.
|
||||
|
||||
New (or deleted) invariant checks must be amendment-gated: they introduce (or remove) a way for a transaction to fail, and an un-gated change risks validators disagreeing on a transaction's result, i.e. a network fork.
|
||||
|
||||
See [transactors/AGENTS.md](./transactors/AGENTS.md) for conventions on writing the amendment-gated code itself.
|
||||
|
||||
A change to transaction/signing behavior that's visible through the public API also needs an `API-CHANGELOG.md` entry — see [../../xrpld/rpc/AGENTS.md](../../xrpld/rpc/AGENTS.md) for the full rule.
|
||||
Any change to transaction-processing behavior must be gated behind an amendment. New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
|
||||
@@ -7,14 +7,12 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -487,7 +485,6 @@ ValidVault::finalize(
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
|
||||
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
|
||||
|
||||
if (!isTesSuccess(ret))
|
||||
return true; // Do not perform checks
|
||||
@@ -1298,18 +1295,6 @@ ValidVault::finalize(
|
||||
|
||||
auto const localPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
|
||||
auto const rate = transferRate(view, vaultAsset);
|
||||
bool const feeAdjustedWithdrawal = fix350Enabled && distinctDestination &&
|
||||
!vaultAsset.native() && tx[sfAccount] != vaultAsset.getIssuer() &&
|
||||
rate != kParityRate;
|
||||
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
|
||||
if (feeAdjustedWithdrawal)
|
||||
{
|
||||
expectedDestinationDelta =
|
||||
subtractTransferFee(
|
||||
STAmount{vaultAsset, expectedDestinationDelta}, rate)
|
||||
.value();
|
||||
}
|
||||
// For IOU assets near a precision boundary the destination's STAmount
|
||||
// exponent can shift, making part of the sent value unrepresentable at
|
||||
// the receiver's new scale — that portion is irreversibly absorbed by the
|
||||
@@ -1318,8 +1303,7 @@ ValidVault::finalize(
|
||||
// the destination's scale. Floor rounding is used so that values exactly
|
||||
// at the step boundary are not mistakenly dismissed. Any representable
|
||||
// discrepancy indicates a real accounting bug and must be caught.
|
||||
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
|
||||
tolerateZeroDelta &&
|
||||
auto const destroyedIsSubUlp = tolerateZeroDelta &&
|
||||
roundToAsset(
|
||||
vaultAsset,
|
||||
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
|
||||
@@ -1327,11 +1311,11 @@ ValidVault::finalize(
|
||||
Number::RoundingMode::Downward) == kZero;
|
||||
bool const withdrawAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
expectedDestinationDelta,
|
||||
localPseudoDeltaAssets * -1,
|
||||
roundedDestinationDelta,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: expectedDestinationDelta == roundedDestinationDelta;
|
||||
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
|
||||
if (!destroyedIsSubUlp && !withdrawAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# AGENTS.md — transactors
|
||||
|
||||
Prefer a single object-level invariant over duplicating the same delta/balance check in every transactor that touches an object — e.g. one invariant asserting a Vault's pseudo-account balance and `assetsAvailable` always move together, rather than repeating that check in `VaultDeposit`, `VaultWithdraw`, `VaultClawback`, `LoanSet`, etc.
|
||||
|
||||
## Gating amendment-dependent code
|
||||
|
||||
Prefer a single amendment-enabled block and a single disabled block over scattering `rules.enabled(...)` checks through a function, even if the two blocks are similar.
|
||||
|
||||
When a file or function checks more than one amendment, name local enablement booleans per-amendment (e.g. `fix340Enabled` for `fixCleanup3_4_0`), not a generic `fixEnabled` — it becomes ambiguous once a second amendment is checked in the same scope.
|
||||
|
||||
## `UNREACHABLE` and test coverage
|
||||
|
||||
Only use `UNREACHABLE` for genuinely impossible paths, not to avoid writing a test for one that's reachable but rare. When a branch marked `UNREACHABLE` is excluded from coverage, wrap it in `LCOV_EXCL_START`/`LCOV_EXCL_STOP`.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -270,7 +270,6 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
brokerPseudoID,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
amount,
|
||||
j_);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,12 @@
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
@@ -52,26 +49,6 @@ shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::Const
|
||||
: WaiveUnrealizedLoss::No;
|
||||
}
|
||||
|
||||
static Rate
|
||||
withdrawalTransferRate(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& destination,
|
||||
Asset const& asset)
|
||||
{
|
||||
// Pre-fixCleanup3_5_0: every vault withdrawal waives the transfer fee.
|
||||
// Post-fixCleanup3_5_0: the fee applies only when a holder other than the issuer withdraws to
|
||||
// another such holder.
|
||||
if (!view.rules().enabled(fixCleanup3_5_0))
|
||||
return kParityRate;
|
||||
|
||||
if (asset.native() || destination == account || account == asset.getIssuer() ||
|
||||
destination == asset.getIssuer())
|
||||
return kParityRate;
|
||||
|
||||
return transferRate(view, asset);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
VaultWithdraw::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -193,13 +170,11 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (!maybeAssets)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const rate = withdrawalTransferRate(ctx.view, account, dstAcct, vaultAsset);
|
||||
auto const amountToReceive = subtractTransferFee(*maybeAssets, rate);
|
||||
if (auto const ret = canWithdraw(
|
||||
ctx.view,
|
||||
account,
|
||||
dstAcct,
|
||||
amountToReceive,
|
||||
*maybeAssets,
|
||||
ctx.tx.isFieldPresent(sfDestinationTag),
|
||||
ctx.tx[~sfCredentialIDs]))
|
||||
return ret;
|
||||
@@ -327,8 +302,6 @@ VaultWithdraw::doApply()
|
||||
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
auto const rate = withdrawalTransferRate(view(), accountID_, dstAcct, vaultAsset);
|
||||
|
||||
MPTIssue const share{mptIssuanceID};
|
||||
STAmount sharesRedeemed = {share};
|
||||
@@ -357,11 +330,7 @@ VaultWithdraw::doApply()
|
||||
view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault,
|
||||
sleIssuance,
|
||||
addTransferFee(amount, rate),
|
||||
truncate,
|
||||
waiveUnrealizedLoss);
|
||||
vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesRedeemed = *maybeShares;
|
||||
@@ -554,13 +523,6 @@ VaultWithdraw::doApply()
|
||||
assetsWithdrawn = allAvailable;
|
||||
}
|
||||
|
||||
auto const assetsDelivered = subtractTransferFee(assetsWithdrawn, rate);
|
||||
if (assetsWithdrawn > beast::kZero && assetsDelivered == beast::kZero)
|
||||
{
|
||||
JLOG(j_.debug()) << "VaultWithdraw: transfer fee reduces the payout to zero";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (finalPayout && getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Do not let dust accumulate in the Vault.
|
||||
@@ -611,15 +573,9 @@ VaultWithdraw::doApply()
|
||||
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
return doWithdraw(
|
||||
applyViewContext,
|
||||
accountID_,
|
||||
dstAcct,
|
||||
vaultAccount,
|
||||
preFeeBalance_,
|
||||
assetsDelivered,
|
||||
assetsWithdrawn,
|
||||
j_);
|
||||
applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# AGENTS.md — test
|
||||
|
||||
See [README.md](./README.md) for basic `--unittest` invocation.
|
||||
|
||||
Shared test setup/helper code used by more than one test file belongs in `jtx/`, not copy-pasted across test files.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -50,39 +50,6 @@ namespace xrpl {
|
||||
class VaultLifecycle_test : public VaultTestBase
|
||||
{
|
||||
private:
|
||||
struct FundedVault
|
||||
{
|
||||
Keylet keylet;
|
||||
test::jtx::Account account;
|
||||
test::jtx::PrettyAsset shares;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a vault owned by `owner` and deposits `amount` into it from `depositor`.
|
||||
*/
|
||||
static FundedVault
|
||||
createFundedVault(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Vault& vault,
|
||||
test::jtx::Account const& owner,
|
||||
test::jtx::Account const& depositor,
|
||||
STAmount const& amount)
|
||||
{
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = amount.asset()});
|
||||
env(tx);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount}));
|
||||
env.close();
|
||||
|
||||
auto const sle = env.le(keylet);
|
||||
test::jtx::Account const account{"vault", sle->at(sfAccount)};
|
||||
env.memoize(account);
|
||||
return {
|
||||
.keylet = keylet,
|
||||
.account = account,
|
||||
.shares = test::jtx::PrettyAsset{MPTIssue{sle->at(sfShareMPTID)}},
|
||||
};
|
||||
}
|
||||
void
|
||||
testSequences()
|
||||
{
|
||||
@@ -606,7 +573,6 @@ private:
|
||||
bool enableClawback = true;
|
||||
bool requireAuth = true;
|
||||
int initialXRP = 1000;
|
||||
std::uint16_t transferFee = 0;
|
||||
FeatureBitset features = testableAmendments();
|
||||
};
|
||||
|
||||
@@ -631,7 +597,6 @@ private:
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
auto const kNone = LedgerSpecificFlags(0);
|
||||
mptt.create({
|
||||
.transferFee = args.transferFee,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanLock |
|
||||
(args.enableClawback ? tfMPTCanClawback : kNone) |
|
||||
(args.requireAuth ? tfMPTRequireAuth : kNone),
|
||||
@@ -651,324 +616,6 @@ private:
|
||||
test(env, issuer, owner, depositor, asset, vault, mptt);
|
||||
};
|
||||
|
||||
// The MPT charges a 25% transfer fee, so 100 gross delivers 80 net.
|
||||
auto const feeArgs = CaseArgs{.transferFee = 25'000};
|
||||
|
||||
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
bool const feeCharged = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
feeCharged ? "MPT transfer fee on third-party withdrawal"
|
||||
: "MPT transfer fee waived pre-fixCleanup3_5_0");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(100),
|
||||
});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const expected = feeCharged ? 80 : 100;
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 900));
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, expected));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(900 + expected));
|
||||
BEAST_EXPECT(
|
||||
env.balance(funded.account, asset.raw().get<MPTIssue>()) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferFee = 25'000, .features = features});
|
||||
};
|
||||
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
|
||||
testTransferFeeGate(testableAmendments());
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee on fixed-asset third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The destination receives the requested 40; the gross 50 leaves the vault.
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 40));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(50));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(50));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee on fixed-share third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(25)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The 25 shares redeem 25 assets; the destination receives 20 net.
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 20));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(75));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(75));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(995));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on withdrawal to issuer");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)});
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(90));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on self-withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 910));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1000));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee must fit in the vault position");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
// Delivering 81 costs 102 gross, more than the 100 shares held.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(81)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx, Ter{tecINSUFFICIENT_FUNDS});
|
||||
env.close();
|
||||
|
||||
// Delivering 80 costs exactly the 100 shares held.
|
||||
tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(80)});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 80));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
|
||||
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(0));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(980));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
// The payout rounds down whether or not MPTokensV2 is enabled.
|
||||
auto const testTransferFeeRoundsDown = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase(
|
||||
features[featureMPTokensV2]
|
||||
? "MPT transfer fee rounds the payout down to zero"
|
||||
: "MPT transfer fee rounds the payout down to zero without "
|
||||
"MPTokensV2");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(1));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(1),
|
||||
});
|
||||
tx[sfDestination] = owner.human();
|
||||
env(tx, Ter{tecPRECISION_LOSS});
|
||||
env.close();
|
||||
|
||||
// A self-withdrawal pays no fee, so the same share still redeems.
|
||||
env(vault.withdraw({
|
||||
.depositor = depositor,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(1),
|
||||
}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1000));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferFee = 25'000, .features = features});
|
||||
};
|
||||
testTransferFeeRoundsDown(testableAmendments());
|
||||
testTransferFeeRoundsDown(testableAmendments() - featureMPTokensV2);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT transfer fee leaves other depositors whole");
|
||||
|
||||
Account const charlie{"charlie"};
|
||||
env.fund(XRP(1000), charlie);
|
||||
env.close();
|
||||
mptt.authorize({.account = charlie});
|
||||
mptt.authorize({.account = issuer, .holder = charlie});
|
||||
env(pay(issuer, owner, asset(100)));
|
||||
env.close();
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
|
||||
env(vault.deposit(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto const vaultBefore = env.le(funded.keylet);
|
||||
Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
|
||||
Number const assetsAvailableBefore = vaultBefore->at(sfAssetsAvailable);
|
||||
|
||||
// Delivering 40 to charlie costs the depositor 50 gross.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(charlie, 40));
|
||||
auto const vaultAfter = env.le(funded.keylet);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - 50);
|
||||
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore - 50);
|
||||
|
||||
// The owner's 100 shares still redeem for the 100 it deposited.
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(100));
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = funded.shares(100)}));
|
||||
env.close();
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
PrettyAsset const& asset,
|
||||
Vault& vault,
|
||||
MPTTester& mptt) {
|
||||
testcase("MPT no transfer fee on issuer withdrawal to a third party");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
|
||||
auto const mptIssue = asset.raw().get<MPTIssue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = depositor.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1040));
|
||||
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(60));
|
||||
BEAST_EXPECT(env.balance(issuer, shareIssue) == funded.shares(60));
|
||||
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase([this](
|
||||
Env& env,
|
||||
Account const& issuer,
|
||||
@@ -1852,281 +1499,66 @@ private:
|
||||
env.close();
|
||||
});
|
||||
|
||||
// The issuer charges a 25% transfer fee, so 100 gross delivers 80 net.
|
||||
auto const feeArgs = CaseArgs{.transferRate = 1.25};
|
||||
testCase(
|
||||
[&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto vaultAccount,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto issuanceId) {
|
||||
testcase("IOU transfer fees not applied");
|
||||
|
||||
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
|
||||
testCase(
|
||||
[this, features](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
bool const feeCharged = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
feeCharged ? "IOU transfer fee on third-party withdrawal"
|
||||
: "IOU transfer fee waived pre-fixCleanup3_5_0");
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
// Deposits are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
Asset const share = Asset(issuanceId(keylet));
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = funded.keylet.key,
|
||||
.holder = owner,
|
||||
.amount = asset(50),
|
||||
}));
|
||||
env.close();
|
||||
// transfer fees ignored on deposit
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
|
||||
|
||||
// Clawbacks are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
|
||||
env(vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(20'000'000),
|
||||
}));
|
||||
env.close();
|
||||
|
||||
// Self-withdrawals are fee-free.
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(30));
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(30'000'000),
|
||||
});
|
||||
tx[sfDestination] = charlie.human();
|
||||
{
|
||||
auto const tx = vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
|
||||
env(tx);
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const expected = feeCharged ? asset(24) : asset(30);
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == expected);
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
CaseArgs{.transferRate = 1.25, .features = features});
|
||||
};
|
||||
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
|
||||
testTransferFeeGate(testableAmendments());
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee on fixed-asset third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The destination receives the requested 40; the gross 50 leaves the vault.
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(50'000'000));
|
||||
// transfer fees ignored on clawback
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee on fixed-share third-party withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
|
||||
|
||||
auto tx = vault.withdraw({
|
||||
.depositor = owner,
|
||||
.id = funded.keylet.key,
|
||||
.amount = funded.shares(25'000'000),
|
||||
});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// The shares redeem 25 assets; the destination receives 20 net.
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(20));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(75));
|
||||
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(75'000'000));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const&,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on withdrawal to issuer");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)});
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const&,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on self-withdrawal");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)}));
|
||||
env.close();
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
|
||||
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(110));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
|
||||
},
|
||||
feeArgs);
|
||||
// transfer fees ignored on withdraw
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU transfer fee must fit in the vault position");
|
||||
{
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
}
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
// transfer fees ignored on withdraw to 3rd party
|
||||
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
|
||||
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
|
||||
|
||||
// Delivering 81 costs 101.25 gross, more than the vault position.
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(81)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx, Ter{tecINSUFFICIENT_FUNDS});
|
||||
env.close();
|
||||
|
||||
// Delivering 80 costs exactly the 100 held.
|
||||
tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(80)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(80));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
|
||||
|
||||
env(vault.del({.owner = owner, .id = funded.keylet.key}));
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU no transfer fee on issuer withdrawal to a third party");
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(60));
|
||||
},
|
||||
feeArgs);
|
||||
|
||||
testCase(
|
||||
[this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const&,
|
||||
Account const& charlie,
|
||||
auto,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU receiver limit applies to the net amount");
|
||||
|
||||
// The limit fits the 40 delivered but not the 50 gross.
|
||||
env(trust(charlie, asset(40)));
|
||||
env.close();
|
||||
|
||||
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
|
||||
auto const issue = asset.raw().get<Issue>();
|
||||
|
||||
auto tx = vault.withdraw(
|
||||
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
|
||||
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
|
||||
},
|
||||
feeArgs);
|
||||
CaseArgs{.transferRate = 1.25});
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// A 25% transfer fee.
|
||||
Rate const kRate125{1'250'000'000};
|
||||
|
||||
AccountID const kIssuer{1};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(TokenHelpers, add_transfer_fee)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kIssuer};
|
||||
MPTIssue const mpt{makeMptID(1, kIssuer)};
|
||||
|
||||
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kRate125), STAmount(usd, 100));
|
||||
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kParityRate), STAmount(usd, 80));
|
||||
|
||||
// Integral MPT: the fee on 1 unit rounds up to a whole unit.
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 80}, kRate125), STAmount(mpt, 100));
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt, 2));
|
||||
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
|
||||
}
|
||||
|
||||
TEST(TokenHelpers, subtract_transfer_fee)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kIssuer};
|
||||
MPTIssue const mpt{makeMptID(1, kIssuer)};
|
||||
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kRate125), STAmount(usd, 80));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kParityRate), STAmount(usd, 100));
|
||||
|
||||
// Integral MPT: the payout rounds down, to zero for a single unit.
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 31}, kRate125), STAmount(mpt, 24));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt));
|
||||
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// A 25% transfer fee.
|
||||
constexpr std::uint32_t kRate125 = 1'250'000'000;
|
||||
|
||||
AccountID const kIssuer{1};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(STAmount, mul_ratio_iou)
|
||||
{
|
||||
Issue const usd{toCurrency("USD"), kIssuer};
|
||||
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{usd, 100}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
|
||||
STAmount(usd, 80));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{usd, 80}, kRate125, QUALITY_ONE, Number::RoundingMode::Upward),
|
||||
STAmount(usd, 100));
|
||||
|
||||
// 1 / 1.7 does not fit in a 16-digit mantissa, so the rounding direction
|
||||
// decides the last digit.
|
||||
STAmount const one{usd, 1};
|
||||
EXPECT_LT(
|
||||
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Downward),
|
||||
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Upward));
|
||||
|
||||
STAmount const full{usd, std::uint64_t{9'999'999'999'999'999}, -5};
|
||||
EXPECT_EQ(mulRatio(full, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), full);
|
||||
}
|
||||
|
||||
TEST(STAmount, mul_ratio_mpt)
|
||||
{
|
||||
MPTIssue const mpt{makeMptID(1, kIssuer)};
|
||||
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
|
||||
STAmount(mpt, 24));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
|
||||
STAmount(mpt, 25));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
|
||||
STAmount(mpt));
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
|
||||
STAmount(mpt, 1));
|
||||
|
||||
STAmount const max{mpt, kMaxMpTokenAmount};
|
||||
EXPECT_EQ(mulRatio(max, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), max);
|
||||
EXPECT_NO_THROW((void)mulRatio(max, QUALITY_ONE, kRate125, Number::RoundingMode::Downward));
|
||||
EXPECT_THROW(
|
||||
(void)mulRatio(max, kRate125, QUALITY_ONE, Number::RoundingMode::Upward),
|
||||
std::overflow_error);
|
||||
}
|
||||
|
||||
TEST(STAmount, mul_ratio_xrp)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{XRPAmount{31}}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
|
||||
STAmount{XRPAmount{24}});
|
||||
EXPECT_EQ(
|
||||
mulRatio(STAmount{XRPAmount{31}}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
|
||||
STAmount{XRPAmount{25}});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — consensus
|
||||
|
||||
See [README.md](./README.md) for a short pointer, and [docs/consensus.md](../../../../docs/consensus.md) for the full consensus design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — ledger
|
||||
|
||||
See [README.md](./README.md) for ledger lifecycle and fetch-pack design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — overlay
|
||||
|
||||
See [README.md](./README.md) for the peer-protocol handshake, clustering, gossip, and monitoring design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,3 +0,0 @@
|
||||
# AGENTS.md — peerfinder
|
||||
|
||||
See [README.md](./README.md) for the peer-discovery design.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,5 +1,5 @@
|
||||
# AGENTS.md — rpc
|
||||
|
||||
See [README.md](./README.md) for the RPC subsystem design.
|
||||
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
|
||||
|
||||
Any change to publicly-visible API behavior — RPC/WebSocket fields, parameters, or error conditions, or transaction/signing behavior surfaced through the API even from outside this directory — needs an entry in [`API-CHANGELOG.md`](../../../API-CHANGELOG.md) under a `## Unreleased` section (`### Additions`, `### Bugfixes`, `### Deprecations`, etc. as appropriate). If that section is absent, create it above the latest `## XRP Ledger server version x.y.z` heading; it is renamed at release.
|
||||
Any change to a public RPC method's behavior (new/changed/removed fields, parameters, or error conditions) needs a corresponding entry in [`API-CHANGELOG.md`](../../../API-CHANGELOG.md), under the `## Unreleased` section (`### Additions`, `### Deprecations`, etc. as appropriate).
|
||||
Reference in new issue
Block a user